github-actions[bot] commited on
Commit
b60040e
·
1 Parent(s): 462a5fb

🚀 Auto-deploy backend from GitHub (a7f0c2e)

Browse files
Files changed (2) hide show
  1. main.py +56 -16
  2. routes/diagnostic.py +84 -5
main.py CHANGED
@@ -116,10 +116,12 @@ except Exception:
116
  try:
117
  from google.oauth2 import id_token as google_id_token # type: ignore[import-not-found]
118
  from google.auth.transport import requests as google_auth_requests # type: ignore[import-not-found]
 
119
  HAS_GOOGLE_AUTH = True
120
  except Exception:
121
  google_id_token = None # type: ignore[assignment]
122
  google_auth_requests = None # type: ignore[assignment]
 
123
  HAS_GOOGLE_AUTH = False
124
 
125
  # Event-driven automation engine
@@ -11328,6 +11330,25 @@ def _testing_reset_try_set_doc(doc_ref: Any, payload: Dict[str, Any], label: str
11328
  return 0
11329
 
11330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11331
  def _reset_student_testing_data_admin(
11332
  client: Any,
11333
  uid: str,
@@ -11354,24 +11375,38 @@ def _reset_student_testing_data_admin(
11354
  merge=False,
11355
  )
11356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11357
  updated_docs += _testing_reset_try_set_doc(
11358
  client.collection("users").document(uid),
11359
- {
11360
- "level": 1,
11361
- "currentXP": 0,
11362
- "totalXP": 0,
11363
- "streak": 0,
11364
- "streakHistory": [],
11365
- "atRiskSubjects": [],
11366
- "hasTakenDiagnostic": False,
11367
- "iarAssessmentState": "not_started",
11368
- "learningPathState": "unlocked",
11369
- "remediationState": "not_required",
11370
- "subjectBadges": {},
11371
- "riskClassifications": {},
11372
- "overallRisk": "Low",
11373
- "updatedAt": timestamp_value,
11374
- },
11375
  f"users/{uid}",
11376
  merge=True,
11377
  )
@@ -11380,6 +11415,11 @@ def _reset_student_testing_data_admin(
11380
  deleted_docs += _testing_reset_try_delete_by_field(client, "chatSessions", "userId", uid)
11381
  deleted_docs += _testing_reset_try_delete_by_field(client, "chatMessages", "userId", uid)
11382
 
 
 
 
 
 
11383
  if effective_lrn != uid:
11384
  deleted_docs += _testing_reset_try_delete_by_field(client, "notifications", "userId", effective_lrn)
11385
 
 
116
  try:
117
  from google.oauth2 import id_token as google_id_token # type: ignore[import-not-found]
118
  from google.auth.transport import requests as google_auth_requests # type: ignore[import-not-found]
119
+ from google.cloud.firestore import DELETE_FIELD # type: ignore[import-not-found]
120
  HAS_GOOGLE_AUTH = True
121
  except Exception:
122
  google_id_token = None # type: ignore[assignment]
123
  google_auth_requests = None # type: ignore[assignment]
124
+ DELETE_FIELD = None # type: ignore[assignment]
125
  HAS_GOOGLE_AUTH = False
126
 
127
  # Event-driven automation engine
 
11330
  return 0
11331
 
11332
 
11333
+ def _testing_reset_try_delete_subcollection(
11334
+ client: Any, parent_path: str, subcollection_name: str
11335
+ ) -> int:
11336
+ """Delete all documents in a subcollection. Returns count of deleted docs."""
11337
+ try:
11338
+ docs = list(client.collection(parent_path).document().parent.collection(subcollection_name).stream())
11339
+ for doc_snapshot in docs:
11340
+ doc_snapshot.reference.delete()
11341
+ return len(docs)
11342
+ except Exception as err:
11343
+ logger.warning(
11344
+ "Testing reset skipped delete for %s/%s: %s",
11345
+ parent_path,
11346
+ subcollection_name,
11347
+ err,
11348
+ )
11349
+ return 0
11350
+
11351
+
11352
  def _reset_student_testing_data_admin(
11353
  client: Any,
11354
  uid: str,
 
11375
  merge=False,
11376
  )
11377
 
11378
+ # Build users/{uid} payload with DELETE_FIELD for optional assessment fields
11379
+ users_payload = {
11380
+ "level": 1,
11381
+ "currentXP": 0,
11382
+ "totalXP": 0,
11383
+ "streak": 0,
11384
+ "streakHistory": [],
11385
+ "atRiskSubjects": [],
11386
+ "hasTakenDiagnostic": False,
11387
+ "iarAssessmentState": "not_started",
11388
+ "learningPathState": "unlocked",
11389
+ "remediationState": "not_required",
11390
+ "subjectBadges": {},
11391
+ "riskClassifications": {},
11392
+ "overallRisk": "Low",
11393
+ "updatedAt": timestamp_value,
11394
+ }
11395
+ # Add assessment-specific fields using DELETE_FIELD to remove them
11396
+ if DELETE_FIELD is not None:
11397
+ users_payload["diagnosticCompleted"] = DELETE_FIELD
11398
+ users_payload["lastAssessmentDate"] = DELETE_FIELD
11399
+ users_payload["assessmentAttemptCount"] = DELETE_FIELD
11400
+ users_payload["initialProficiencyLevel"] = DELETE_FIELD
11401
+ else:
11402
+ users_payload["diagnosticCompleted"] = False
11403
+ users_payload["lastAssessmentDate"] = None
11404
+ users_payload["assessmentAttemptCount"] = 0
11405
+ users_payload["initialProficiencyLevel"] = None
11406
+
11407
  updated_docs += _testing_reset_try_set_doc(
11408
  client.collection("users").document(uid),
11409
+ users_payload,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11410
  f"users/{uid}",
11411
  merge=True,
11412
  )
 
11415
  deleted_docs += _testing_reset_try_delete_by_field(client, "chatSessions", "userId", uid)
11416
  deleted_docs += _testing_reset_try_delete_by_field(client, "chatMessages", "userId", uid)
11417
 
11418
+ # Delete assessment subcollection documents
11419
+ deleted_docs += _testing_reset_try_delete_subcollection(client, f"assessmentResults/{uid}", "attempts")
11420
+ deleted_docs += _testing_reset_try_delete_subcollection(client, f"studentProgress/{uid}", "diagnostics")
11421
+ deleted_docs += _testing_reset_try_delete_subcollection(client, f"assessmentQuestionHistory/{uid}", "questions")
11422
+
11423
  if effective_lrn != uid:
11424
  deleted_docs += _testing_reset_try_delete_by_field(client, "notifications", "userId", effective_lrn)
11425
 
routes/diagnostic.py CHANGED
@@ -9,6 +9,7 @@ from __future__ import annotations
9
  import asyncio
10
  import json
11
  import logging
 
12
  import traceback
13
  import uuid
14
  from collections import defaultdict
@@ -224,7 +225,33 @@ def _build_rag_context(strand: str) -> str:
224
  return "\n".join(rag_context_parts)
225
 
226
 
227
- def _build_system_prompt(strand: str, grade_level: str, rag_context: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  strand_upper = strand.upper()
229
  coverage_text = _get_strand_coverage(strand_upper)
230
 
@@ -240,6 +267,26 @@ reference material. If the reference material is insufficient for a topic,
240
  use only standard DepEd SHS competencies for that strand.
241
  """
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  return f"""SYSTEM ROLE:
244
  You are MathPulse AI's Diagnostic Test Generator. Your job is to create a
245
  15-item multiple-choice diagnostic assessment for a Filipino SHS student,
@@ -269,7 +316,7 @@ Statistics: SP-RV-01, SP-RV-02, SP-NORM-01, SP-NORM-02,
269
  SP-SAMP-01, SP-SAMP-03, SP-HYP-01
270
  Finite Math: FM1-MAT-01, FM2-PROB-01, FM2-PROB-02
271
 
272
- DIFFICULTY DISTRIBUTION (across all 15 questions):
273
  - Easy (Bloom: remembering / understanding): 6 questions (40%)
274
  - Medium (Bloom: applying / analyzing): 6 questions (40%)
275
  - Hard (Bloom: evaluating / creating): 3 questions (20%)
@@ -350,10 +397,40 @@ def _parse_questions_response(raw_response: str) -> List[Dict[str, Any]]:
350
  raise ValueError("Could not parse questions from AI response")
351
 
352
 
353
- async def _generate_questions(strand: str, grade_level: str) -> tuple[str, List[Dict[str, Any]]]:
 
 
 
 
 
354
  test_id = f"DX-{uuid.uuid4().hex[:12]}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  rag_context = _build_rag_context(strand)
356
- system_prompt = _build_system_prompt(strand, grade_level, rag_context)
 
 
 
 
 
 
357
  user_message = f"Generate 15 diagnostic questions for a Grade 11 {strand} student."
358
 
359
  for attempt in range(2):
@@ -426,9 +503,12 @@ async def generate_diagnostic(request: DiagnosticGenerateRequest, req: Request):
426
  raise HTTPException(status_code=401, detail="Authentication required")
427
 
428
  try:
 
429
  test_id, questions = await _generate_questions(
430
  request.strand,
431
  request.grade_level,
 
 
432
  )
433
  except HTTPException:
434
  raise
@@ -437,7 +517,6 @@ async def generate_diagnostic(request: DiagnosticGenerateRequest, req: Request):
437
  raise HTTPException(status_code=500, detail="Assessment generation failed. Please try again.")
438
 
439
  try:
440
- firestore_client = fs.client()
441
  stored = await _store_diagnostic_session(
442
  firestore_client,
443
  user.uid,
 
9
  import asyncio
10
  import json
11
  import logging
12
+ import time
13
  import traceback
14
  import uuid
15
  from collections import defaultdict
 
225
  return "\n".join(rag_context_parts)
226
 
227
 
228
+ async def _get_previous_questions(
229
+ user_id: str,
230
+ firestore_client,
231
+ max_attempts: int = 3,
232
+ ) -> list[str]:
233
+ """Fetch question texts from the user's last N assessment attempts to avoid duplicates."""
234
+ try:
235
+ attempts_ref = (
236
+ firestore_client.collection("assessmentResults")
237
+ .document(user_id)
238
+ .collection("attempts")
239
+ .order_by("completedAt", direction=fs.Query.DESCENDING)
240
+ .limit(max_attempts)
241
+ )
242
+ docs = attempts_ref.stream()
243
+ previous_texts: list[str] = []
244
+ for doc in docs:
245
+ data = doc.to_dict()
246
+ answers = data.get("answers", [])
247
+ for a in answers:
248
+ previous_texts.append(a.get("questionText", ""))
249
+ return previous_texts
250
+ except Exception:
251
+ return []
252
+
253
+
254
+ def _build_system_prompt(strand: str, grade_level: str, rag_context: str, variance_seed: int = 0, previous_questions: list[str] | None = None) -> str:
255
  strand_upper = strand.upper()
256
  coverage_text = _get_strand_coverage(strand_upper)
257
 
 
267
  use only standard DepEd SHS competencies for that strand.
268
  """
269
 
270
+ previous_block = ""
271
+ if previous_questions:
272
+ previous_lines = [
273
+ "PREVIOUS QUESTIONS TO AVOID (DO NOT REPEAT):",
274
+ "The following questions were already asked to this student.",
275
+ "You MUST NOT reuse or rephrase any of these:",
276
+ ]
277
+ for i, q in enumerate(previous_questions[:20], 1):
278
+ previous_lines.append(f"{i}. {q}")
279
+ previous_block = "\n".join(previous_lines) + "\n\n"
280
+
281
+ variance_block = ""
282
+ if variance_seed > 0:
283
+ variance_block = (
284
+ f"VARIANCE SEED: {variance_seed}\n"
285
+ "To ensure unique questions, use this seed to generate DIFFERENT "
286
+ "numerical values, problem contexts, and variable names compared "
287
+ "to the standard template.\n\n"
288
+ )
289
+
290
  return f"""SYSTEM ROLE:
291
  You are MathPulse AI's Diagnostic Test Generator. Your job is to create a
292
  15-item multiple-choice diagnostic assessment for a Filipino SHS student,
 
316
  SP-SAMP-01, SP-SAMP-03, SP-HYP-01
317
  Finite Math: FM1-MAT-01, FM2-PROB-01, FM2-PROB-02
318
 
319
+ {previous_block}{variance_block}DIFFICULTY DISTRIBUTION (across all 15 questions):
320
  - Easy (Bloom: remembering / understanding): 6 questions (40%)
321
  - Medium (Bloom: applying / analyzing): 6 questions (40%)
322
  - Hard (Bloom: evaluating / creating): 3 questions (20%)
 
397
  raise ValueError("Could not parse questions from AI response")
398
 
399
 
400
+ async def _generate_questions(
401
+ strand: str,
402
+ grade_level: str,
403
+ user_id: str = "",
404
+ firestore_client=None,
405
+ ) -> tuple[str, List[Dict[str, Any]]]:
406
  test_id = f"DX-{uuid.uuid4().hex[:12]}"
407
+
408
+ # Generate variance seed based on user's attempt count and fetch previous questions
409
+ variance_seed = 0
410
+ previous_questions: list[str] = []
411
+
412
+ if firestore_client and user_id:
413
+ try:
414
+ attempts_ref = (
415
+ firestore_client.collection("assessmentResults")
416
+ .document(user_id)
417
+ .collection("attempts")
418
+ )
419
+ attempts = attempts_ref.stream()
420
+ attempt_count = sum(1 for _ in attempts)
421
+ variance_seed = int(time.time()) % 10000 + attempt_count * 137
422
+ previous_questions = await _get_previous_questions(user_id, firestore_client)
423
+ except Exception:
424
+ pass
425
+
426
  rag_context = _build_rag_context(strand)
427
+ system_prompt = _build_system_prompt(
428
+ strand,
429
+ grade_level,
430
+ rag_context,
431
+ variance_seed=variance_seed,
432
+ previous_questions=previous_questions,
433
+ )
434
  user_message = f"Generate 15 diagnostic questions for a Grade 11 {strand} student."
435
 
436
  for attempt in range(2):
 
503
  raise HTTPException(status_code=401, detail="Authentication required")
504
 
505
  try:
506
+ firestore_client = fs.client()
507
  test_id, questions = await _generate_questions(
508
  request.strand,
509
  request.grade_level,
510
+ user_id=user.uid,
511
+ firestore_client=firestore_client,
512
  )
513
  except HTTPException:
514
  raise
 
517
  raise HTTPException(status_code=500, detail="Assessment generation failed. Please try again.")
518
 
519
  try:
 
520
  stored = await _store_diagnostic_session(
521
  firestore_client,
522
  user.uid,