Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
b60040e
1
Parent(s): 462a5fb
🚀 Auto-deploy backend from GitHub (a7f0c2e)
Browse files- main.py +56 -16
- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
test_id = f"DX-{uuid.uuid4().hex[:12]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
rag_context = _build_rag_context(strand)
|
| 356 |
-
system_prompt = _build_system_prompt(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|