github-actions[bot] commited on
Commit
61efd60
·
1 Parent(s): 84a3491

🚀 Auto-deploy backend from GitHub (7fe6d9b)

Browse files
rag/firebase_storage_loader.py CHANGED
@@ -5,7 +5,6 @@ Downloads PDFs from Firebase Storage and extracts text for ChromaDB indexing.
5
 
6
  from __future__ import annotations
7
 
8
- import datetime
9
  import logging
10
  import os
11
  from pathlib import Path
@@ -149,72 +148,4 @@ PDF_METADATA: Dict[str, dict] = {
149
  "quarter": 1,
150
  "storage_path": "curriculum/stat_prob/SDO_Navotas_STAT_PROB_SHS_1stSem.FV.pdf",
151
  },
152
- }
153
-
154
-
155
- def generate_signed_download_url(storage_path: str, expiration_hours: int = 24) -> Optional[str]:
156
- """Generate a signed download URL for a Firebase Storage blob.
157
-
158
- Args:
159
- storage_path: The path of the blob in Firebase Storage.
160
- expiration_hours: Number of hours until the URL expires (default 24).
161
-
162
- Returns:
163
- Signed URL string, or None if Firebase Storage is unavailable.
164
- """
165
- _, bucket = _init_firebase_storage()
166
- if bucket is None:
167
- logger.warning("Firebase Storage not available, cannot generate signed URL")
168
- return None
169
-
170
- try:
171
- blob = bucket.blob(storage_path)
172
- signed_url = blob.generate_signed_url(
173
- expiration=datetime.timedelta(hours=expiration_hours),
174
- method="GET",
175
- )
176
- logger.info("Generated signed URL for %s (expires in %dh)", storage_path, expiration_hours)
177
- return signed_url
178
- except Exception as e:
179
- logger.error("Failed to generate signed URL for %s: %s", storage_path, e)
180
- return None
181
-
182
-
183
- def get_study_materials_from_chunks(chunks: list[dict]) -> list[dict]:
184
- """Extract study materials from chunks, deduplicating by source PDF.
185
-
186
- Args:
187
- chunks: List of chunk dicts with optional `source_file`, `storage_path`,
188
- and `content_domain` keys.
189
-
190
- Returns:
191
- List of dicts with keys: `title`, `source_pdf_url`, `topic_match`.
192
- """
193
- seen_sources: set[str] = set()
194
- materials: list[dict] = []
195
-
196
- for chunk in chunks:
197
- source = chunk.get("source_file")
198
- if not source or source in seen_sources:
199
- continue
200
- seen_sources.add(source)
201
-
202
- # Look up PDF metadata by storage_path
203
- metadata = PDF_METADATA.get(source)
204
- if metadata:
205
- title = metadata.get("subject", source)
206
- topic_match = metadata.get("content_domain", chunk.get("content_domain", ""))
207
- else:
208
- title = source.split("/")[-1]
209
- topic_match = chunk.get("content_domain", "")
210
-
211
- storage_path = chunk.get("storage_path", source)
212
- source_pdf_url = generate_signed_download_url(storage_path)
213
-
214
- materials.append({
215
- "title": title,
216
- "source_pdf_url": source_pdf_url or "",
217
- "topic_match": topic_match,
218
- })
219
-
220
- return materials
 
5
 
6
  from __future__ import annotations
7
 
 
8
  import logging
9
  import os
10
  from pathlib import Path
 
148
  "quarter": 1,
149
  "storage_path": "curriculum/stat_prob/SDO_Navotas_STAT_PROB_SHS_1stSem.FV.pdf",
150
  },
151
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
routes/rag_routes.py CHANGED
@@ -1,6 +1,5 @@
1
  from __future__ import annotations
2
 
3
- import asyncio
4
  import json
5
  import logging
6
  import os
@@ -28,7 +27,6 @@ from rag.curriculum_rag import (
28
  retrieve_lesson_pdf_context,
29
  summarize_retrieval_confidence,
30
  )
31
- from rag.firebase_storage_loader import get_study_materials_from_chunks
32
  from rag.vectorstore_loader import get_vectorstore_health, reset_vectorstore_singleton
33
 
34
  try:
@@ -72,57 +70,6 @@ async def _generate_text(
72
  return _get_inference_client().generate_from_messages(request)
73
 
74
 
75
- _FLASHCARD_SYSTEM_PROMPT = """You are an educational flashcard generator for Filipino high school mathematics students (DepEd K-12 curriculum).
76
- Given a lesson text, generate exactly 10 flashcards in JSON format.
77
- Each flashcard has:
78
- - "front": a concise question, term, or problem prompt (max 20 words)
79
- - "back": the answer, definition, or solution (max 40 words)
80
- - "difficulty": one of "easy", "medium", or "hard"
81
-
82
- Distribute difficulty: 3 easy, 4 medium, 3 hard.
83
- Focus on key concepts, formulas, definitions, and problem-solving steps from the lesson.
84
- Return ONLY a valid JSON array. No markdown, no explanation."""
85
-
86
-
87
- async def _generate_flashcards(lesson_text: str, topic: str) -> List[dict]:
88
- """Generate 10 flashcards from lesson content using DeepSeek AI."""
89
- user_message = f"Topic: {topic}\n\nLesson content:\n{lesson_text}"
90
- request = InferenceRequest(
91
- messages=[
92
- {"role": "system", "content": _FLASHCARD_SYSTEM_PROMPT},
93
- {"role": "user", "content": user_message},
94
- ],
95
- task_type="flashcard_generation",
96
- max_new_tokens=800,
97
- temperature=0.3,
98
- )
99
- try:
100
- raw_response = await asyncio.to_thread(
101
- _get_inference_client().generate_from_messages, request
102
- )
103
- # Strip markdown fences if present
104
- cleaned = raw_response.strip()
105
- if cleaned.startswith("```"):
106
- lines = cleaned.splitlines()
107
- cleaned = "".join(lines[1:-1])
108
- parsed = json.loads(cleaned)
109
- if not isinstance(parsed, list):
110
- logger.warning("Flashcard response was not a list")
111
- return []
112
- validated = []
113
- for card in parsed:
114
- if isinstance(card, dict) and all(k in card for k in ("front", "back", "difficulty")):
115
- validated.append({
116
- "front": str(card.get("front", "")),
117
- "back": str(card.get("back", "")),
118
- "difficulty": str(card.get("difficulty", "medium")),
119
- })
120
- return validated
121
- except Exception as exc:
122
- logger.warning("Flashcard generation failed: %s", exc)
123
- return []
124
-
125
-
126
  def _log_rag_usage(
127
  request: Request,
128
  *,
@@ -156,51 +103,6 @@ def _log_rag_usage(
156
  logger.warning("rag_usage logging skipped: %s", exc)
157
 
158
 
159
- def _get_cached_generated_assets(lesson_id: str, topic_slug: str) -> Optional[dict]:
160
- """Return cached study_materials + flashcards if they exist and are fresh (≤7 days)."""
161
- if firebase_firestore is None:
162
- return None
163
- try:
164
- doc_ref = firebase_firestore.client().collection("lessons").document(lesson_id).collection("generated_assets").document(topic_slug)
165
- doc = doc_ref.get()
166
- if not doc.exists:
167
- return None
168
- data = doc.to_dict()
169
- generated_at = data.get("generated_at")
170
- if generated_at is None:
171
- return None
172
- # Firestore.Timestamp → datetime comparison
173
- try:
174
- ts = generated_at.replace(tzinfo=datetime.now(timezone.utc).tzinfo) if hasattr(generated_at, "replace") else None
175
- except Exception:
176
- ts = None
177
- if ts is None:
178
- # Fallback: assume fresh if we can't parse
179
- return {"study_materials": data.get("study_materials"), "flashcards": data.get("flashcards")}
180
- age_seconds = (datetime.now(timezone.utc) - ts).total_seconds()
181
- if age_seconds > 604800: # 7 days
182
- return None
183
- return {"study_materials": data.get("study_materials"), "flashcards": data.get("flashcards")}
184
- except Exception as exc:
185
- logger.warning("cached_generated_assets lookup skipped: %s", exc)
186
- return None
187
-
188
-
189
- def _save_generated_assets(lesson_id: str, topic_slug: str, study_materials: list, flashcards: list) -> None:
190
- """Persist study_materials + flashcards to Firestore for future reuse."""
191
- if firebase_firestore is None:
192
- return
193
- try:
194
- doc_ref = firebase_firestore.client().collection("lessons").document(lesson_id).collection("generated_assets").document(topic_slug)
195
- doc_ref.set({
196
- "study_materials": study_materials,
197
- "flashcards": flashcards,
198
- "generated_at": firebase_firestore.SERVER_TIMESTAMP,
199
- })
200
- except Exception as exc:
201
- logger.warning("cached_generated_assets save skipped: %s", exc)
202
-
203
-
204
  def _strip_thinking_and_parse(text: str) -> dict:
205
  cleaned = text.strip()
206
  cleaned = re.sub(r" </think>", "", cleaned, flags=re.DOTALL).strip()
@@ -364,12 +266,6 @@ def _ensure_7_sections(lesson_data: dict, lesson_title: str) -> dict:
364
 
365
  @router.post("/lesson")
366
  async def rag_lesson(request: Request, payload: RagLessonRequest):
367
- # ── Step 0: Check Firestore cache ────────────────────────────────────────
368
- topic_slug = f"{payload.subject}_{payload.quarter}_{payload.topic}"
369
- cached_assets = _get_cached_generated_assets(payload.lessonId or payload.topic, topic_slug)
370
- if cached_assets:
371
- logger.info("Cache hit for generated_assets: lesson_id=%s, topic_slug=%s", payload.lessonId or payload.topic, topic_slug)
372
-
373
  # ── Step 1: Retrieve curriculum chunks ───────────────────────────────────
374
  try:
375
  chunks, retrieval_mode = retrieve_lesson_pdf_context(
@@ -415,9 +311,6 @@ async def rag_lesson(request: Request, payload: RagLessonRequest):
415
  },
416
  )
417
 
418
- # Extract study materials from retrieved chunks
419
- study_materials = get_study_materials_from_chunks(chunks)
420
-
421
  # ── Step 2: Build prompt ─────────────────────────────────────────────────
422
  try:
423
  prompt = build_lesson_prompt(
@@ -476,26 +369,18 @@ async def rag_lesson(request: Request, payload: RagLessonRequest):
476
  },
477
  )
478
 
479
- # ── Step 5: Enrich with videos + generate flashcards concurrently ────────
480
- flashcards = []
481
  if parsed_lesson.get("sections"):
482
  video_section = next((s for s in parsed_lesson["sections"] if s.get("type") == "video"), None)
483
  if video_section:
484
  try:
485
- # Run video fetch and flashcard generation concurrently
486
- video_task = asyncio.to_thread(
487
- _fetch_youtube_videos,
488
  payload.lessonTitle or payload.topic,
489
  payload.subject,
490
  payload.learningCompetency or "",
491
  payload.quarter,
492
- payload.lessonId,
493
  )
494
- flashcard_task = _generate_flashcards(
495
- json.dumps(parsed_lesson),
496
- payload.lessonTitle or payload.topic,
497
- )
498
- videos, flashcards = await asyncio.gather(video_task, flashcard_task)
499
  if videos:
500
  # Primary video for backwards compatibility
501
  primary = videos[0]
@@ -507,7 +392,7 @@ async def rag_lesson(request: Request, payload: RagLessonRequest):
507
  # New: full videos array for Smart Video Integration
508
  video_section["videos"] = videos
509
  except Exception as exc:
510
- logger.warning("YouTube/flashcard enrichment skipped: %s", exc)
511
 
512
  # ── Step 6: Assemble response ────────────────────────────────────────────
513
  retrieval_summary = summarize_retrieval_confidence(chunks)
@@ -528,21 +413,6 @@ async def rag_lesson(request: Request, payload: RagLessonRequest):
528
  if retrieval_summary.get("band") == "low":
529
  needs_review = True
530
 
531
- # Use cached assets if available, otherwise save newly generated ones
532
- if cached_assets:
533
- study_materials = cached_assets.get("study_materials") or study_materials
534
- flashcards = cached_assets.get("flashcards") or flashcards
535
- else:
536
- try:
537
- _save_generated_assets(
538
- payload.lessonId or payload.topic,
539
- topic_slug,
540
- study_materials,
541
- flashcards,
542
- )
543
- except Exception as exc:
544
- logger.warning("Generated assets cache save skipped: %s", exc)
545
-
546
  return {
547
  **parsed_lesson,
548
  "retrievalConfidence": retrieval_summary.get("confidence", 0.0),
@@ -564,8 +434,6 @@ async def rag_lesson(request: Request, payload: RagLessonRequest):
564
  for row in chunks
565
  ],
566
  "activeModel": get_model_for_task("rag_lesson"),
567
- "study_materials": study_materials,
568
- "flashcards": flashcards,
569
  }
570
 
571
 
 
1
  from __future__ import annotations
2
 
 
3
  import json
4
  import logging
5
  import os
 
27
  retrieve_lesson_pdf_context,
28
  summarize_retrieval_confidence,
29
  )
 
30
  from rag.vectorstore_loader import get_vectorstore_health, reset_vectorstore_singleton
31
 
32
  try:
 
70
  return _get_inference_client().generate_from_messages(request)
71
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def _log_rag_usage(
74
  request: Request,
75
  *,
 
103
  logger.warning("rag_usage logging skipped: %s", exc)
104
 
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  def _strip_thinking_and_parse(text: str) -> dict:
107
  cleaned = text.strip()
108
  cleaned = re.sub(r" </think>", "", cleaned, flags=re.DOTALL).strip()
 
266
 
267
  @router.post("/lesson")
268
  async def rag_lesson(request: Request, payload: RagLessonRequest):
 
 
 
 
 
 
269
  # ── Step 1: Retrieve curriculum chunks ───────────────────────────────────
270
  try:
271
  chunks, retrieval_mode = retrieve_lesson_pdf_context(
 
311
  },
312
  )
313
 
 
 
 
314
  # ── Step 2: Build prompt ─────────────────────────────────────────────────
315
  try:
316
  prompt = build_lesson_prompt(
 
369
  },
370
  )
371
 
372
+ # ── Step 5: Enrich with videos ───────────────────────────────────────────
 
373
  if parsed_lesson.get("sections"):
374
  video_section = next((s for s in parsed_lesson["sections"] if s.get("type") == "video"), None)
375
  if video_section:
376
  try:
377
+ videos = _fetch_youtube_videos(
 
 
378
  payload.lessonTitle or payload.topic,
379
  payload.subject,
380
  payload.learningCompetency or "",
381
  payload.quarter,
382
+ lesson_id=payload.lessonId,
383
  )
 
 
 
 
 
384
  if videos:
385
  # Primary video for backwards compatibility
386
  primary = videos[0]
 
392
  # New: full videos array for Smart Video Integration
393
  video_section["videos"] = videos
394
  except Exception as exc:
395
+ logger.warning("YouTube enrichment skipped: %s", exc)
396
 
397
  # ── Step 6: Assemble response ────────────────────────────────────────────
398
  retrieval_summary = summarize_retrieval_confidence(chunks)
 
413
  if retrieval_summary.get("band") == "low":
414
  needs_review = True
415
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  return {
417
  **parsed_lesson,
418
  "retrievalConfidence": retrieval_summary.get("confidence", 0.0),
 
434
  for row in chunks
435
  ],
436
  "activeModel": get_model_for_task("rag_lesson"),
 
 
437
  }
438
 
439
 
tests/test_rag_pipeline.py CHANGED
@@ -153,166 +153,4 @@ class TestIsSequentialModel:
153
  def test_not_sequential_for_chat(self):
154
  with patch.dict(os.environ, {"INFERENCE_MODEL_ID": "deepseek-chat"}):
155
  from services.inference_client import is_sequential_model
156
- assert is_sequential_model() is False
157
-
158
-
159
- class TestGenerateFlashcards:
160
- def _make_flashcard(self, front: str, back: str, difficulty: str) -> dict:
161
- return {"front": front, "back": back, "difficulty": difficulty}
162
-
163
- def _build_flashcard_response(self) -> str:
164
- cards = [
165
- self._make_flashcard("What is the compound interest formula?", "A = P(1 + r/n)^(nt)", "easy"),
166
- self._make_flashcard("Define principal in interest calculations.", "The initial amount of money borrowed or invested.", "easy"),
167
- self._make_flashcard("What does n represent in compound interest?", "The number of times interest is compounded per year.", "easy"),
168
- self._make_flashcard("How is nominal rate different from effective rate?", "Nominal is the stated rate; effective accounts for compounding.", "medium"),
169
- self._make_flashcard("Calculate A if P=1000, r=5%, n=4, t=2 years.", "A = 1000(1 + 0.05/4)^(4*2) = 1000(1.0125)^8 ≈ 1104.49", "medium"),
170
- self._make_flashcard("What happens when compounding frequency increases?", "The effective rate approaches but never exceeds the nominal rate.", "medium"),
171
- self._make_flashcard("Derive the compound interest formula from simple interest.", "Start with A = P(1 + rt) and extend to continuous compounding.", "medium"),
172
- self._make_flashcard("When should you use logarithms in compound interest problems?", "When solving for time t given A, P, r, and n.", "hard"),
173
- self._make_flashcard("Compare future value vs present value in investment decisions.", "FV shows growth; PV shows today's worth of future money.", "hard"),
174
- self._make_flashcard("Solve for t if A = 2P with annual compounding at rate r.", "t = ln(2) / ln(1 + r). Requires natural log application.", "hard"),
175
- ]
176
- import json
177
- return json.dumps(cards)
178
-
179
- def test_generate_flashcards_returns_ten_cards(self):
180
- mock_client = MagicMock()
181
- mock_client.generate_from_messages.return_value = self._build_flashcard_response()
182
-
183
- with patch("routes.rag_routes._get_inference_client", return_value=mock_client):
184
- from routes.rag_routes import _generate_flashcards
185
- import asyncio
186
- result = asyncio.run(_generate_flashcards("lesson text here", "Compound Interest"))
187
-
188
- assert len(result) == 10
189
- for card in result:
190
- assert "front" in card
191
- assert "back" in card
192
- assert "difficulty" in card
193
-
194
- def test_generate_flashcards_difficulty_distribution(self):
195
- mock_client = MagicMock()
196
- mock_client.generate_from_messages.return_value = self._build_flashcard_response()
197
-
198
- with patch("routes.rag_routes._get_inference_client", return_value=mock_client):
199
- from routes.rag_routes import _generate_flashcards
200
- import asyncio
201
- result = asyncio.run(_generate_flashcards("lesson text here", "Compound Interest"))
202
-
203
- difficulties = [c["difficulty"] for c in result]
204
- assert difficulties.count("easy") == 3
205
- assert difficulties.count("medium") == 4
206
- assert difficulties.count("hard") == 3
207
-
208
- def test_generate_flashcards_returns_empty_on_exception(self):
209
- mock_client = MagicMock()
210
- mock_client.generate_from_messages.side_effect = Exception("AI inference failed")
211
-
212
- with patch("routes.rag_routes._get_inference_client", return_value=mock_client):
213
- from routes.rag_routes import _generate_flashcards
214
- import asyncio
215
- result = asyncio.run(_generate_flashcards("lesson text here", "Compound Interest"))
216
-
217
- assert result == []
218
-
219
-
220
- class TestGetStudyMaterialsFromChunks:
221
- def test_deduplicates_by_source_file(self):
222
- chunks = [
223
- {"source_file": "curriculum/gen_math_sdo/SDO_Navotas_Gen.Math_SHS_1stSem.FV.pdf", "storage_path": "curriculum/gen_math_sdo/SDO_Navotas_Gen.Math_SHS_1stSem.FV.pdf", "content_domain": "general"},
224
- {"source_file": "curriculum/gen_math_sdo/SDO_Navotas_Gen.Math_SHS_1stSem.FV.pdf", "storage_path": "curriculum/gen_math_sdo/SDO_Navotas_Gen.Math_SHS_1stSem.FV.pdf", "content_domain": "general"},
225
- {"source_file": "curriculum/business_math/SDO_Navotas_Bus.Math_SHS_1stSem.FV.pdf", "storage_path": "curriculum/business_math/SDO_Navotas_Bus.Math_SHS_1stSem.FV.pdf", "content_domain": "business"},
226
- ]
227
-
228
- with patch("rag.firebase_storage_loader.generate_signed_download_url", return_value="https://storage.example.com/signed"):
229
- from rag.firebase_storage_loader import get_study_materials_from_chunks
230
- result = get_study_materials_from_chunks(chunks)
231
-
232
- assert len(result) == 2 # deduplicated
233
-
234
- def test_each_material_has_title_source_pdf_url_topic_match(self):
235
- chunks = [
236
- {"source_file": "curriculum/gen_math_sdo/SDO_Navotas_Gen.Math_SHS_1stSem.FV.pdf", "storage_path": "curriculum/gen_math_sdo/SDO_Navotas_Gen.Math_SHS_1stSem.FV.pdf", "content_domain": "general"},
237
- ]
238
-
239
- with patch("rag.firebase_storage_loader.generate_signed_download_url", return_value="https://storage.example.com/signed"):
240
- from rag.firebase_storage_loader import get_study_materials_from_chunks
241
- result = get_study_materials_from_chunks(chunks)
242
-
243
- assert len(result) == 1
244
- mat = result[0]
245
- assert "title" in mat
246
- assert "source_pdf_url" in mat
247
- assert "topic_match" in mat
248
- assert mat["source_pdf_url"] == "https://storage.example.com/signed"
249
-
250
-
251
- class TestRagLessonExtendedResponse:
252
- @pytest.mark.skip(reason="Requires auth middleware setup; tested manually")
253
- def test_response_includes_study_materials_and_flashcards(self):
254
- mock_chunks = [
255
- {
256
- "subject": "General Mathematics",
257
- "quarter": 1,
258
- "source_file": "curriculum/gen_math_sdo/SDO_Navotas_Gen.Math_SHS_1stSem.FV.pdf",
259
- "storage_path": "curriculum/gen_math_sdo/SDO_Navotas_Gen.Math_SHS_1stSem.FV.pdf",
260
- "page": 5,
261
- "score": 0.85,
262
- "content_domain": "general",
263
- "chunk_type": "content_explanation",
264
- "content": "Compound interest formula A=P(1+r/n)^(nt)",
265
- }
266
- ]
267
-
268
- mock_lesson_response = {
269
- "explanation": "Lesson on compound interest",
270
- "needsReview": False,
271
- "sections": [
272
- {"type": "introduction", "title": "Introduction", "content": "Welcome to compound interest."},
273
- {"type": "key_concepts", "title": "Key Concepts", "content": "Key concepts here."},
274
- {"type": "video", "title": "Video Lesson", "content": "Video content.", "videoId": "", "videoTitle": "", "videoChannel": "", "embedUrl": "", "thumbnailUrl": ""},
275
- {"type": "worked_examples", "title": "Worked Examples", "examples": []},
276
- {"type": "important_notes", "title": "Important Notes", "bulletPoints": []},
277
- {"type": "try_it_yourself", "title": "Try It Yourself", "practiceProblems": []},
278
- {"type": "summary", "title": "Summary", "content": "Summary content."},
279
- ],
280
- }
281
-
282
- mock_client = MagicMock()
283
- mock_client.generate_from_messages.return_value = '{"explanation":"Lesson on compound interest","needsReview":false}'
284
-
285
- with patch("rag.curriculum_rag.retrieve_lesson_pdf_context", return_value=(mock_chunks, "chroma")):
286
- with patch("rag.curriculum_rag.build_lesson_prompt", return_value="test prompt"):
287
- with patch("routes.rag_routes._get_inference_client", return_value=mock_client):
288
- with patch("routes.rag_routes._generate_flashcards", return_value=[]):
289
- with patch("rag.firebase_storage_loader.generate_signed_download_url", return_value="https://storage.example.com/signed"):
290
- with patch("routes.rag_routes._get_cached_generated_assets", return_value=None):
291
- with patch("routes.rag_routes._save_generated_assets"):
292
- from fastapi.testclient import TestClient
293
- from main import app
294
-
295
- # Inject mock user for _log_rag_usage
296
- mock_user = MagicMock()
297
- mock_user.uid = "test-user"
298
- type(mock_user).uid = property(lambda self: "test-user")
299
-
300
- client = TestClient(app)
301
- response = client.post(
302
- "/api/rag/lesson",
303
- json={
304
- "topic": "Compound Interest",
305
- "subject": "General Mathematics",
306
- "quarter": 1,
307
- "lessonTitle": "Compound Interest Basics",
308
- "learningCompetency": "M11GM-IIc-1",
309
- },
310
- )
311
-
312
- assert response.status_code == 200
313
- data = response.json()
314
- assert "study_materials" in data
315
- assert "flashcards" in data
316
- # Ensure materials were extracted from chunks
317
- assert isinstance(data["study_materials"], list)
318
- assert isinstance(data["flashcards"], list)
 
153
  def test_not_sequential_for_chat(self):
154
  with patch.dict(os.environ, {"INFERENCE_MODEL_ID": "deepseek-chat"}):
155
  from services.inference_client import is_sequential_model
156
+ assert is_sequential_model() is False