Spaces:
Sleeping
Sleeping
github-actions[bot] commited on
Commit ·
e388cbf
1
Parent(s): ee201de
deploy: backend from b8e1988
Browse files- main.py +57 -14
- tests/test_api.py +59 -1
- tests/test_chat_jev_socratic.py +23 -0
- tests/test_student_account_import.py +30 -0
main.py
CHANGED
|
@@ -350,6 +350,10 @@ CHAT_STREAM_CONTINUATION_TAIL_CHARS = max(
|
|
| 350 |
80,
|
| 351 |
int(os.getenv("CHAT_STREAM_CONTINUATION_TAIL_CHARS", "900")),
|
| 352 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
CHAT_STREAM_COMPLETION_MODE_DEFAULT = os.getenv(
|
| 354 |
"CHAT_STREAM_COMPLETION_MODE_DEFAULT",
|
| 355 |
"auto",
|
|
@@ -2421,6 +2425,27 @@ async def root():
|
|
| 2421 |
# ─── AI Chat Tutor ─────────────────────────────────────────────
|
| 2422 |
|
| 2423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2424 |
MATH_TUTOR_SYSTEM_PROMPT = """You are Pulse, MathPulse AI's friendly math tutor for Filipino Senior High School
|
| 2425 |
students. You help students understand and solve problems in General Mathematics,
|
| 2426 |
Business Mathematics, Statistics & Probability, and Finite Mathematics, all aligned
|
|
@@ -2429,6 +2454,7 @@ with the DepEd Strengthened SHS Curriculum and SDO Navotas learning modules.
|
|
| 2429 |
YOUR BEHAVIOR RULES:
|
| 2430 |
1. PERSONALIZE every response. Address the student by first name occasionally.
|
| 2431 |
2. NEVER give direct answers to quiz or exam items — guide with hints and questions instead.
|
|
|
|
| 2432 |
3. If the student is struggling on a critical gap topic, gently steer them back to
|
| 2433 |
prerequisite concepts before moving forward.
|
| 2434 |
4. Use the SDO Navotas step-by-step method for ALL solutions:
|
|
@@ -2709,13 +2735,14 @@ def _build_stream_continuation_prompt(original_question: str, expected_end_marke
|
|
| 2709 |
|
| 2710 |
@app.post("/api/chat", response_model=ChatResponse)
|
| 2711 |
async def chat_tutor_endpoint(request: Request, chat_request: ChatRequest):
|
| 2712 |
-
return await chat_tutor(chat_request)
|
| 2713 |
|
| 2714 |
|
| 2715 |
-
async def chat_tutor(request: ChatRequest):
|
| 2716 |
"""AI Math Tutor powered by Hugging Face Inference routing."""
|
| 2717 |
_start_ms = int(time.monotonic() * 1000)
|
| 2718 |
try:
|
|
|
|
| 2719 |
# ─── Context-Aware Intent Gate (before scope check) ──────
|
| 2720 |
# Load active state to determine if student is mid-session.
|
| 2721 |
# If so, skip scope check for short/vague replies.
|
|
@@ -2912,11 +2939,12 @@ async def _update_memory_after_response(
|
|
| 2912 |
|
| 2913 |
@app.post("/api/chat/stream")
|
| 2914 |
async def chat_tutor_stream_endpoint(request: Request, chat_request: ChatRequest):
|
| 2915 |
-
return await chat_tutor_stream(chat_request)
|
| 2916 |
|
| 2917 |
|
| 2918 |
-
async def chat_tutor_stream(request: ChatRequest):
|
| 2919 |
"""SSE stream endpoint for AI Math Tutor chat responses."""
|
|
|
|
| 2920 |
try:
|
| 2921 |
# ─── Context-Aware Intent Gate (before scope check) ──────
|
| 2922 |
_skip_scope_check = False
|
|
@@ -5333,6 +5361,23 @@ def _generate_temporary_password(length: int = 12) -> str:
|
|
| 5333 |
return candidate
|
| 5334 |
|
| 5335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5336 |
def _parse_provisioning_dataframe(df: Any) -> Dict[str, Any]:
|
| 5337 |
header_map: Dict[str, str] = {}
|
| 5338 |
for column in df.columns.tolist():
|
|
@@ -6049,7 +6094,7 @@ async def preview_student_account_import(
|
|
| 6049 |
email = str(row.get("email") or "").strip().lower()
|
| 6050 |
grade = str(row.get("grade") or "").strip() or effective_default_grade
|
| 6051 |
section = str(row.get("section") or "").strip() or effective_default_section
|
| 6052 |
-
full_name =
|
| 6053 |
|
| 6054 |
issues: List[str] = []
|
| 6055 |
if not first_name:
|
|
@@ -6255,12 +6300,8 @@ async def commit_student_account_import(
|
|
| 6255 |
if str(row.get("status") or "") in {"valid", "move_confirmation_required"}
|
| 6256 |
]
|
| 6257 |
|
| 6258 |
-
|
| 6259 |
-
|
| 6260 |
-
for row in preview_rows
|
| 6261 |
-
if str(row.get("studentId") or "").strip()
|
| 6262 |
-
]
|
| 6263 |
-
duplicate_lrns = {lrn for lrn, count in Counter(student_lrns).items() if count > 1}
|
| 6264 |
has_existing_lrn = any(
|
| 6265 |
bool(row.get("duplicateLrnInFirestore"))
|
| 6266 |
and str(row.get("status") or "") != "move_confirmation_required"
|
|
@@ -11974,6 +12015,10 @@ Remember:
|
|
| 11974 |
detail=f"Quiz generation returned {len(parsed_questions)} of {request.numQuestions} requested questions. Please try again.",
|
| 11975 |
)
|
| 11976 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11977 |
topic_provenance_map: Dict[str, Dict[str, Optional[str]]] = {}
|
| 11978 |
for imported_topic in (imported_topics_payload.get("topics") or []):
|
| 11979 |
title = str(imported_topic.get("title") or "").strip()
|
|
@@ -12077,11 +12122,9 @@ Remember:
|
|
| 12077 |
@app.post("/api/quiz/preview", response_model=QuizResponse)
|
| 12078 |
async def preview_quiz(http_request: Request, request: QuizGenerationRequest):
|
| 12079 |
"""
|
| 12080 |
-
Generate a
|
| 12081 |
quality before assigning a full quiz to students.
|
| 12082 |
"""
|
| 12083 |
-
# Override to produce only 3 questions
|
| 12084 |
-
request.numQuestions = 3
|
| 12085 |
return await generate_quiz(http_request, request)
|
| 12086 |
|
| 12087 |
|
|
|
|
| 350 |
80,
|
| 351 |
int(os.getenv("CHAT_STREAM_CONTINUATION_TAIL_CHARS", "900")),
|
| 352 |
)
|
| 353 |
+
CHAT_RATE_LIMIT_REQUESTS = max(1, int(os.getenv("CHAT_RATE_LIMIT_REQUESTS", "60")))
|
| 354 |
+
CHAT_RATE_LIMIT_WINDOW_SECONDS = max(1, int(os.getenv("CHAT_RATE_LIMIT_WINDOW_SECONDS", "60")))
|
| 355 |
+
_chat_rate_limit_requests: Dict[str, List[float]] = defaultdict(list)
|
| 356 |
+
_chat_rate_limit_lock = Lock()
|
| 357 |
CHAT_STREAM_COMPLETION_MODE_DEFAULT = os.getenv(
|
| 358 |
"CHAT_STREAM_COMPLETION_MODE_DEFAULT",
|
| 359 |
"auto",
|
|
|
|
| 2425 |
# ─── AI Chat Tutor ─────────────────────────────────────────────
|
| 2426 |
|
| 2427 |
|
| 2428 |
+
def _enforce_chat_rate_limit(http_request: Optional[Request]) -> None:
|
| 2429 |
+
user = getattr(getattr(http_request, "state", None), "user", None)
|
| 2430 |
+
client = getattr(http_request, "client", None)
|
| 2431 |
+
user_key = getattr(user, "uid", None) or getattr(client, "host", None) or "anonymous"
|
| 2432 |
+
now = time.monotonic()
|
| 2433 |
+
cutoff = now - CHAT_RATE_LIMIT_WINDOW_SECONDS
|
| 2434 |
+
with _chat_rate_limit_lock:
|
| 2435 |
+
recent_requests = [
|
| 2436 |
+
timestamp for timestamp in _chat_rate_limit_requests[user_key]
|
| 2437 |
+
if timestamp > cutoff
|
| 2438 |
+
]
|
| 2439 |
+
if len(recent_requests) >= CHAT_RATE_LIMIT_REQUESTS:
|
| 2440 |
+
_chat_rate_limit_requests[user_key] = recent_requests
|
| 2441 |
+
raise HTTPException(
|
| 2442 |
+
status_code=429,
|
| 2443 |
+
detail="You are asking too fast, please wait a moment",
|
| 2444 |
+
)
|
| 2445 |
+
recent_requests.append(now)
|
| 2446 |
+
_chat_rate_limit_requests[user_key] = recent_requests
|
| 2447 |
+
|
| 2448 |
+
|
| 2449 |
MATH_TUTOR_SYSTEM_PROMPT = """You are Pulse, MathPulse AI's friendly math tutor for Filipino Senior High School
|
| 2450 |
students. You help students understand and solve problems in General Mathematics,
|
| 2451 |
Business Mathematics, Statistics & Probability, and Finite Mathematics, all aligned
|
|
|
|
| 2454 |
YOUR BEHAVIOR RULES:
|
| 2455 |
1. PERSONALIZE every response. Address the student by first name occasionally.
|
| 2456 |
2. NEVER give direct answers to quiz or exam items — guide with hints and questions instead.
|
| 2457 |
+
On a student's first response to a math problem, do not give the final answer: withhold the final numeric answer and ask a guiding question first. Do not state a solved value such as "x = -3" in that first response. Reveal and explain the answer only after the student has attempted the next step or explicitly asks to check their work.
|
| 2458 |
3. If the student is struggling on a critical gap topic, gently steer them back to
|
| 2459 |
prerequisite concepts before moving forward.
|
| 2460 |
4. Use the SDO Navotas step-by-step method for ALL solutions:
|
|
|
|
| 2735 |
|
| 2736 |
@app.post("/api/chat", response_model=ChatResponse)
|
| 2737 |
async def chat_tutor_endpoint(request: Request, chat_request: ChatRequest):
|
| 2738 |
+
return await chat_tutor(chat_request, request)
|
| 2739 |
|
| 2740 |
|
| 2741 |
+
async def chat_tutor(request: ChatRequest, http_request: Request = None):
|
| 2742 |
"""AI Math Tutor powered by Hugging Face Inference routing."""
|
| 2743 |
_start_ms = int(time.monotonic() * 1000)
|
| 2744 |
try:
|
| 2745 |
+
_enforce_chat_rate_limit(http_request)
|
| 2746 |
# ─── Context-Aware Intent Gate (before scope check) ──────
|
| 2747 |
# Load active state to determine if student is mid-session.
|
| 2748 |
# If so, skip scope check for short/vague replies.
|
|
|
|
| 2939 |
|
| 2940 |
@app.post("/api/chat/stream")
|
| 2941 |
async def chat_tutor_stream_endpoint(request: Request, chat_request: ChatRequest):
|
| 2942 |
+
return await chat_tutor_stream(chat_request, request)
|
| 2943 |
|
| 2944 |
|
| 2945 |
+
async def chat_tutor_stream(request: ChatRequest, http_request: Request = None):
|
| 2946 |
"""SSE stream endpoint for AI Math Tutor chat responses."""
|
| 2947 |
+
_enforce_chat_rate_limit(http_request)
|
| 2948 |
try:
|
| 2949 |
# ─── Context-Aware Intent Gate (before scope check) ──────
|
| 2950 |
_skip_scope_check = False
|
|
|
|
| 5361 |
return candidate
|
| 5362 |
|
| 5363 |
|
| 5364 |
+
def _student_import_full_name(row: Dict[str, Any]) -> str:
|
| 5365 |
+
return " ".join(
|
| 5366 |
+
part for part in (str(row.get("firstName") or "").strip(), str(row.get("lastName") or "").strip()) if part
|
| 5367 |
+
)
|
| 5368 |
+
|
| 5369 |
+
|
| 5370 |
+
def _duplicate_student_lrn_rows(rows: Sequence[Dict[str, Any]]) -> Dict[str, List[int]]:
|
| 5371 |
+
row_numbers_by_lrn: Dict[str, List[int]] = {}
|
| 5372 |
+
for row in rows:
|
| 5373 |
+
# Normalize keys consistently with the import rejection gate: strip all
|
| 5374 |
+
# whitespace and lowercase so variants of the same LRN always collide.
|
| 5375 |
+
student_id = re.sub(r"\s+", "", str(row.get("studentId") or "")).lower()
|
| 5376 |
+
if student_id:
|
| 5377 |
+
row_numbers_by_lrn.setdefault(student_id, []).append(int(row.get("rowNumber") or 0))
|
| 5378 |
+
return {student_id: row_numbers for student_id, row_numbers in row_numbers_by_lrn.items() if len(row_numbers) > 1}
|
| 5379 |
+
|
| 5380 |
+
|
| 5381 |
def _parse_provisioning_dataframe(df: Any) -> Dict[str, Any]:
|
| 5382 |
header_map: Dict[str, str] = {}
|
| 5383 |
for column in df.columns.tolist():
|
|
|
|
| 6094 |
email = str(row.get("email") or "").strip().lower()
|
| 6095 |
grade = str(row.get("grade") or "").strip() or effective_default_grade
|
| 6096 |
section = str(row.get("section") or "").strip() or effective_default_section
|
| 6097 |
+
full_name = _student_import_full_name(row)
|
| 6098 |
|
| 6099 |
issues: List[str] = []
|
| 6100 |
if not first_name:
|
|
|
|
| 6300 |
if str(row.get("status") or "") in {"valid", "move_confirmation_required"}
|
| 6301 |
]
|
| 6302 |
|
| 6303 |
+
duplicate_lrn_rows = _duplicate_student_lrn_rows(preview_rows)
|
| 6304 |
+
duplicate_lrns = set(duplicate_lrn_rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6305 |
has_existing_lrn = any(
|
| 6306 |
bool(row.get("duplicateLrnInFirestore"))
|
| 6307 |
and str(row.get("status") or "") != "move_confirmation_required"
|
|
|
|
| 12015 |
detail=f"Quiz generation returned {len(parsed_questions)} of {request.numQuestions} requested questions. Please try again.",
|
| 12016 |
)
|
| 12017 |
|
| 12018 |
+
parsed_questions = parsed_questions[:request.numQuestions]
|
| 12019 |
+
for question_index, question in enumerate(parsed_questions):
|
| 12020 |
+
question["topic"] = effective_topics[question_index % len(effective_topics)]
|
| 12021 |
+
|
| 12022 |
topic_provenance_map: Dict[str, Dict[str, Optional[str]]] = {}
|
| 12023 |
for imported_topic in (imported_topics_payload.get("topics") or []):
|
| 12024 |
title = str(imported_topic.get("title") or "").strip()
|
|
|
|
| 12122 |
@app.post("/api/quiz/preview", response_model=QuizResponse)
|
| 12123 |
async def preview_quiz(http_request: Request, request: QuizGenerationRequest):
|
| 12124 |
"""
|
| 12125 |
+
Generate a requested-count preview quiz for teachers to verify AI question
|
| 12126 |
quality before assigning a full quiz to students.
|
| 12127 |
"""
|
|
|
|
|
|
|
| 12128 |
return await generate_quiz(http_request, request)
|
| 12129 |
|
| 12130 |
|
tests/test_api.py
CHANGED
|
@@ -855,6 +855,60 @@ class TestQuizGeneration:
|
|
| 855 |
})
|
| 856 |
assert response.status_code == 422
|
| 857 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
|
| 859 |
class TestClassRecordImportMapping:
|
| 860 |
def test_sanitize_column_mapping_drops_none_and_unknown_fields(self):
|
|
@@ -897,14 +951,16 @@ class TestClassRecordImportMapping:
|
|
| 897 |
"points": 1,
|
| 898 |
"explanation": "Slope = rise/run.",
|
| 899 |
}
|
| 900 |
-
for index in range(
|
| 901 |
])
|
| 902 |
mock_chat.return_value = quiz_json
|
| 903 |
response = client.post("/api/quiz/preview", json={
|
| 904 |
"topics": ["Algebra"],
|
| 905 |
"gradeLevel": "Grade 11",
|
|
|
|
| 906 |
})
|
| 907 |
assert response.status_code == 200
|
|
|
|
| 908 |
|
| 909 |
@patch("main.call_hf_chat")
|
| 910 |
def test_preview_quiz_rejects_partial_question_set(self, mock_chat):
|
|
@@ -926,6 +982,7 @@ class TestClassRecordImportMapping:
|
|
| 926 |
response = client.post("/api/quiz/preview", json={
|
| 927 |
"topics": ["Algebra"],
|
| 928 |
"gradeLevel": "Grade 11",
|
|
|
|
| 929 |
})
|
| 930 |
|
| 931 |
assert response.status_code == 502
|
|
@@ -950,6 +1007,7 @@ class TestClassRecordImportMapping:
|
|
| 950 |
response = client.post("/api/quiz/preview", json={
|
| 951 |
"topics": ["Algebra"],
|
| 952 |
"gradeLevel": "Grade 11",
|
|
|
|
| 953 |
})
|
| 954 |
|
| 955 |
assert response.status_code == 502
|
|
|
|
| 855 |
})
|
| 856 |
assert response.status_code == 422
|
| 857 |
|
| 858 |
+
@patch("main.call_hf_chat")
|
| 859 |
+
def test_generate_quiz_returns_requested_count_for_selected_topic(self, mock_chat):
|
| 860 |
+
questions = [
|
| 861 |
+
{
|
| 862 |
+
"questionType": "identification",
|
| 863 |
+
"question": f"Linear equation question {index + 1}",
|
| 864 |
+
"correctAnswer": "x = -3",
|
| 865 |
+
"topic": "Algebra",
|
| 866 |
+
}
|
| 867 |
+
for index in range(5)
|
| 868 |
+
]
|
| 869 |
+
mock_chat.return_value = json.dumps(questions)
|
| 870 |
+
|
| 871 |
+
response = client.post("/api/quiz/preview", json={
|
| 872 |
+
"topics": ["Linear Equations"],
|
| 873 |
+
"gradeLevel": "Grade 11",
|
| 874 |
+
"numQuestions": 5,
|
| 875 |
+
})
|
| 876 |
+
|
| 877 |
+
assert response.status_code == 200
|
| 878 |
+
generated = response.json()["questions"]
|
| 879 |
+
assert len(generated) == 5
|
| 880 |
+
assert all(question["topic"] == "Linear Equations" for question in generated)
|
| 881 |
+
prompt = mock_chat.call_args.args[0]
|
| 882 |
+
assert "Linear Equations" in str(prompt)
|
| 883 |
+
|
| 884 |
+
@patch("main.call_hf_chat")
|
| 885 |
+
def test_chat_rate_limit_returns_429_after_flood(self, mock_chat, monkeypatch):
|
| 886 |
+
mock_chat.return_value = "What is the first step?"
|
| 887 |
+
monkeypatch.setattr(main_module, "CHAT_RATE_LIMIT_REQUESTS", 2)
|
| 888 |
+
monkeypatch.setattr(main_module, "CHAT_RATE_LIMIT_WINDOW_SECONDS", 60)
|
| 889 |
+
main_module._chat_rate_limit_requests.clear()
|
| 890 |
+
|
| 891 |
+
payload = {"message": "How do I solve x + 2 = 5?", "history": []}
|
| 892 |
+
assert client.post("/api/chat", json=payload).status_code == 200
|
| 893 |
+
assert client.post("/api/chat", json=payload).status_code == 200
|
| 894 |
+
limited = client.post("/api/chat", json=payload)
|
| 895 |
+
|
| 896 |
+
assert limited.status_code == 429
|
| 897 |
+
assert "You are asking too fast, please wait a moment" in limited.json()["detail"]
|
| 898 |
+
|
| 899 |
+
|
| 900 |
+
def test_ai_monitoring_summary_exposes_nested_telemetry_without_top_level_counters():
|
| 901 |
+
from routes import ai_monitoring
|
| 902 |
+
|
| 903 |
+
summary = ai_monitoring.get_monitoring_summary(_admin=object())
|
| 904 |
+
|
| 905 |
+
assert "telemetry" in summary
|
| 906 |
+
assert "dailyQuestions" not in summary
|
| 907 |
+
assert "averageLatencyMs" not in summary
|
| 908 |
+
assert "successRate" not in summary
|
| 909 |
+
assert "features" in summary
|
| 910 |
+
assert "pricingMeta" in summary
|
| 911 |
+
|
| 912 |
|
| 913 |
class TestClassRecordImportMapping:
|
| 914 |
def test_sanitize_column_mapping_drops_none_and_unknown_fields(self):
|
|
|
|
| 951 |
"points": 1,
|
| 952 |
"explanation": "Slope = rise/run.",
|
| 953 |
}
|
| 954 |
+
for index in range(5)
|
| 955 |
])
|
| 956 |
mock_chat.return_value = quiz_json
|
| 957 |
response = client.post("/api/quiz/preview", json={
|
| 958 |
"topics": ["Algebra"],
|
| 959 |
"gradeLevel": "Grade 11",
|
| 960 |
+
"numQuestions": 5,
|
| 961 |
})
|
| 962 |
assert response.status_code == 200
|
| 963 |
+
assert len(response.json()["questions"]) == 5
|
| 964 |
|
| 965 |
@patch("main.call_hf_chat")
|
| 966 |
def test_preview_quiz_rejects_partial_question_set(self, mock_chat):
|
|
|
|
| 982 |
response = client.post("/api/quiz/preview", json={
|
| 983 |
"topics": ["Algebra"],
|
| 984 |
"gradeLevel": "Grade 11",
|
| 985 |
+
"numQuestions": 3,
|
| 986 |
})
|
| 987 |
|
| 988 |
assert response.status_code == 502
|
|
|
|
| 1007 |
response = client.post("/api/quiz/preview", json={
|
| 1008 |
"topics": ["Algebra"],
|
| 1009 |
"gradeLevel": "Grade 11",
|
| 1010 |
+
"numQuestions": 3,
|
| 1011 |
})
|
| 1012 |
|
| 1013 |
assert response.status_code == 502
|
tests/test_chat_jev_socratic.py
CHANGED
|
@@ -74,6 +74,29 @@ async def test_conceptual_question_uses_normal_chat_generation(
|
|
| 74 |
assert "Given → Formula → Substitute → Compute → Conclude" in system_prompt
|
| 75 |
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
@pytest.mark.asyncio
|
| 78 |
async def test_jev_failure_fails_open_to_normal_chat_generation(
|
| 79 |
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
| 74 |
assert "Given → Formula → Substitute → Compute → Conclude" in system_prompt
|
| 75 |
|
| 76 |
|
| 77 |
+
@pytest.mark.asyncio
|
| 78 |
+
async def test_chat_system_prompt_requires_guidance_before_final_numeric_answer(
|
| 79 |
+
monkeypatch: pytest.MonkeyPatch,
|
| 80 |
+
) -> None:
|
| 81 |
+
captured_messages: list[dict[str, str]] = []
|
| 82 |
+
|
| 83 |
+
async def route_intent(_message: str) -> dict[str, object]:
|
| 84 |
+
return {"choice": "conceptual_confusion", "confidence": 0.91}
|
| 85 |
+
|
| 86 |
+
async def capture_llm(messages: list[dict[str, str]], **_kwargs: object) -> str:
|
| 87 |
+
captured_messages.extend(messages)
|
| 88 |
+
return "What operation would isolate x first?"
|
| 89 |
+
|
| 90 |
+
monkeypatch.setattr(main, "route_student_intent", route_intent)
|
| 91 |
+
monkeypatch.setattr(main, "call_hf_chat_async", capture_llm)
|
| 92 |
+
|
| 93 |
+
await main.chat_tutor(main.ChatRequest(message="What is the answer to 3x+9=0?"))
|
| 94 |
+
|
| 95 |
+
system_prompt = captured_messages[0]["content"].lower()
|
| 96 |
+
assert "do not give the final answer" in system_prompt
|
| 97 |
+
assert "ask a guiding question first" in system_prompt
|
| 98 |
+
|
| 99 |
+
|
| 100 |
@pytest.mark.asyncio
|
| 101 |
async def test_jev_failure_fails_open_to_normal_chat_generation(
|
| 102 |
monkeypatch: pytest.MonkeyPatch,
|
tests/test_student_account_import.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
|
| 3 |
+
import main
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_provisioning_uses_first_and_last_name_not_middle_initial():
|
| 7 |
+
parsed = main._parse_provisioning_dataframe(
|
| 8 |
+
pd.DataFrame(
|
| 9 |
+
[{"First Name": "Avery", "Middle Initial": "B", "Last Name": "Santos", "LRN": "123456789012"}]
|
| 10 |
+
)
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
row = parsed["rows"][0]
|
| 14 |
+
assert main._student_import_full_name(row) == "Avery Santos"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_duplicate_lrns_report_source_rows_and_reject_entire_batch():
|
| 18 |
+
parsed = main._parse_provisioning_dataframe(
|
| 19 |
+
pd.DataFrame(
|
| 20 |
+
[
|
| 21 |
+
{"First Name": "Avery", "Last Name": "Santos", "LRN": "123456789012"},
|
| 22 |
+
{"First Name": "Blair", "Last Name": "Reyes", "LRN": "123456789012"},
|
| 23 |
+
{"First Name": "Casey", "Last Name": "Lim", "LRN": "210987654321"},
|
| 24 |
+
]
|
| 25 |
+
)
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
assert main._duplicate_student_lrn_rows(parsed["rows"]) == {
|
| 29 |
+
"123456789012": [2, 3]
|
| 30 |
+
}
|