Spaces:
Running
Running
fix: pass client-supplied records/performanceData to Gemini in attendance-analysis and student-insights endpoints
Browse files
main.py
CHANGED
|
@@ -324,41 +324,57 @@ def ai_student_insights():
|
|
| 324 |
term_id = data.get('termId', 'current')
|
| 325 |
include_recs = data.get('includeRecommendations', True)
|
| 326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
system_instruction = (
|
| 328 |
"You are an educational AI analyst. Analyse student performance data and return "
|
| 329 |
"a structured JSON report. Be empathetic, data-driven and actionable. "
|
| 330 |
"CRITICAL: You do not have access to a database. Never invent, fabricate, or "
|
| 331 |
"hallucinate student names, database IDs, scores, or subject data. "
|
| 332 |
-
"Use
|
| 333 |
-
"In the summary field, refer to the student by their studentId only β never invent a name
|
| 334 |
-
"
|
| 335 |
-
"
|
| 336 |
)
|
| 337 |
|
| 338 |
prompt = f"""Generate a student performance insights report as JSON.
|
| 339 |
|
| 340 |
Student ID: {student_id}
|
|
|
|
| 341 |
School ID: {school_id}
|
| 342 |
-
Subjects filter: {subjects if subjects else 'not specified
|
| 343 |
Term: {term_id}
|
| 344 |
Include recommendations: {include_recs}
|
| 345 |
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
- Only include subjectBreakdown entries for subjects explicitly listed above.
|
| 349 |
-
- If no real performance data is available, set averageScore to null and explain in the insight field.
|
| 350 |
-
- Do not fabricate trends, scores, or risk levels.
|
| 351 |
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
{{
|
| 354 |
"studentId": "{student_id}",
|
| 355 |
-
"summary": "<2-3 sentence overview
|
| 356 |
"subjectBreakdown": [
|
| 357 |
{{
|
| 358 |
-
"subject": "<subject
|
| 359 |
"trend": "<improving|stable|declining|unknown>",
|
| 360 |
-
"averageScore": <number 0-100 or null
|
| 361 |
-
"insight": "<specific insight
|
| 362 |
"riskLevel": "<low|medium|high|unknown>"
|
| 363 |
}}
|
| 364 |
],
|
|
@@ -368,8 +384,7 @@ Return ONLY valid JSON matching this exact structure:
|
|
| 368 |
"""
|
| 369 |
result = gemini_json(prompt, system_instruction=system_instruction)
|
| 370 |
|
| 371 |
-
# Sanitise:
|
| 372 |
-
# not a hallucinated value
|
| 373 |
if result:
|
| 374 |
result['studentId'] = student_id
|
| 375 |
|
|
@@ -885,15 +900,25 @@ def ai_attendance_analysis():
|
|
| 885 |
if scope in ('class', 'student') and not scope_id:
|
| 886 |
return jsonify({'error': 'scopeId is required when scope is "class" or "student"'}), 400
|
| 887 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
system_instruction = (
|
| 889 |
"You are an educational data analyst specialising in attendance patterns. "
|
| 890 |
"Identify at-risk students and actionable patterns. "
|
| 891 |
-
"CRITICAL: You do not have access to a database.
|
| 892 |
-
"
|
| 893 |
-
"
|
| 894 |
-
"If real attendance data is
|
| 895 |
-
"
|
| 896 |
-
"Never expose raw database document IDs in
|
| 897 |
"Return only JSON."
|
| 898 |
)
|
| 899 |
|
|
@@ -904,41 +929,45 @@ Scope: {scope} (school | class | student)
|
|
| 904 |
Scope ID: {scope_id or 'N/A'}
|
| 905 |
Date Range: {json.dumps(date_range) if date_range else 'current term'}
|
| 906 |
|
| 907 |
-
|
| 908 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 909 |
- Do not fabricate attendance percentages or absence counts.
|
| 910 |
-
- If attendance records are not included in this prompt, return empty patterns and atRiskStudents arrays.
|
| 911 |
- Never expose raw database document IDs in any text field (summary, detail, recommendation).
|
|
|
|
| 912 |
|
| 913 |
Return ONLY valid JSON:
|
| 914 |
{{
|
| 915 |
"scope": "{scope}",
|
| 916 |
"scopeId": "{scope_id or ''}",
|
| 917 |
-
"period": {json.dumps(date_range) if date_range else '{
|
| 918 |
-
"overallRate": <percentage or null if no
|
| 919 |
"patterns": [
|
| 920 |
{{
|
| 921 |
"type": "<day_of_week|chronic_absence|weather_related|etc>",
|
| 922 |
-
"detail": "<description based only on provided
|
| 923 |
"severity": "<low|medium|high>",
|
| 924 |
-
"studentIds": ["<id
|
| 925 |
}}
|
| 926 |
],
|
| 927 |
"atRiskStudents": [
|
| 928 |
{{
|
| 929 |
-
"studentId": "<id
|
| 930 |
-
"studentName": "<name
|
| 931 |
-
"absenceRate": <
|
| 932 |
"trend": "<improving|stable|worsening>",
|
| 933 |
"recommendation": "<action>"
|
| 934 |
}}
|
| 935 |
],
|
| 936 |
-
"summary": "<overall summary
|
| 937 |
}}
|
| 938 |
"""
|
| 939 |
result = gemini_json(prompt, system_instruction=system_instruction)
|
| 940 |
|
| 941 |
-
# Sanitise: pin scope fields to request values
|
| 942 |
if result:
|
| 943 |
result['scope'] = scope
|
| 944 |
result['scopeId'] = scope_id or ''
|
|
|
|
| 324 |
term_id = data.get('termId', 'current')
|
| 325 |
include_recs = data.get('includeRecommendations', True)
|
| 326 |
|
| 327 |
+
# Extract any performance data the client supplies
|
| 328 |
+
performance_data = (
|
| 329 |
+
data.get('performanceData')
|
| 330 |
+
or data.get('grades')
|
| 331 |
+
or data.get('scores')
|
| 332 |
+
or data.get('results')
|
| 333 |
+
or []
|
| 334 |
+
)
|
| 335 |
+
student_name = data.get('studentName') or data.get('student_name', '')
|
| 336 |
+
|
| 337 |
+
has_data = bool(performance_data)
|
| 338 |
+
|
| 339 |
system_instruction = (
|
| 340 |
"You are an educational AI analyst. Analyse student performance data and return "
|
| 341 |
"a structured JSON report. Be empathetic, data-driven and actionable. "
|
| 342 |
"CRITICAL: You do not have access to a database. Never invent, fabricate, or "
|
| 343 |
"hallucinate student names, database IDs, scores, or subject data. "
|
| 344 |
+
"Use ONLY data explicitly provided in this prompt. "
|
| 345 |
+
"In the summary field, refer to the student by their studentId only β never invent a name "
|
| 346 |
+
"unless studentName is explicitly provided. "
|
| 347 |
+
"In subjectBreakdown, derive scores and trends strictly from the performanceData records below."
|
| 348 |
)
|
| 349 |
|
| 350 |
prompt = f"""Generate a student performance insights report as JSON.
|
| 351 |
|
| 352 |
Student ID: {student_id}
|
| 353 |
+
Student Name: {student_name if student_name else 'NOT PROVIDED β refer to student by ID only'}
|
| 354 |
School ID: {school_id}
|
| 355 |
+
Subjects filter: {subjects if subjects else 'not specified'}
|
| 356 |
Term: {term_id}
|
| 357 |
Include recommendations: {include_recs}
|
| 358 |
|
| 359 |
+
PERFORMANCE DATA (use these records for all analysis β do NOT invent scores):
|
| 360 |
+
{json.dumps(performance_data, ensure_ascii=False) if has_data else 'NO PERFORMANCE DATA PROVIDED β set all scores to null and explain in summary.'}
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
+
STRICT RULES:
|
| 363 |
+
- Use the studentId "{student_id}" exactly as provided.
|
| 364 |
+
- Derive ALL scores, trends, and risk levels from the performance records above only.
|
| 365 |
+
- If a subject appears in the subjects filter but has no records, set averageScore to null.
|
| 366 |
+
- Do not fabricate any numbers, names, or trends.
|
| 367 |
+
|
| 368 |
+
Return ONLY valid JSON:
|
| 369 |
{{
|
| 370 |
"studentId": "{student_id}",
|
| 371 |
+
"summary": "<2-3 sentence overview based only on provided data>",
|
| 372 |
"subjectBreakdown": [
|
| 373 |
{{
|
| 374 |
+
"subject": "<subject name>",
|
| 375 |
"trend": "<improving|stable|declining|unknown>",
|
| 376 |
+
"averageScore": <number 0-100 or null>,
|
| 377 |
+
"insight": "<specific insight from provided records>",
|
| 378 |
"riskLevel": "<low|medium|high|unknown>"
|
| 379 |
}}
|
| 380 |
],
|
|
|
|
| 384 |
"""
|
| 385 |
result = gemini_json(prompt, system_instruction=system_instruction)
|
| 386 |
|
| 387 |
+
# Sanitise: pin studentId to the request value
|
|
|
|
| 388 |
if result:
|
| 389 |
result['studentId'] = student_id
|
| 390 |
|
|
|
|
| 900 |
if scope in ('class', 'student') and not scope_id:
|
| 901 |
return jsonify({'error': 'scopeId is required when scope is "class" or "student"'}), 400
|
| 902 |
|
| 903 |
+
# Extract attendance records the client supplies (several possible key names)
|
| 904 |
+
records = (
|
| 905 |
+
data.get('records')
|
| 906 |
+
or data.get('attendanceData')
|
| 907 |
+
or data.get('attendanceRecords')
|
| 908 |
+
or data.get('data')
|
| 909 |
+
or []
|
| 910 |
+
)
|
| 911 |
+
has_records = bool(records)
|
| 912 |
+
|
| 913 |
system_instruction = (
|
| 914 |
"You are an educational data analyst specialising in attendance patterns. "
|
| 915 |
"Identify at-risk students and actionable patterns. "
|
| 916 |
+
"CRITICAL: You do not have access to a database. "
|
| 917 |
+
"Use ONLY the attendance records explicitly provided in the prompt below. "
|
| 918 |
+
"Never invent, fabricate, or hallucinate student names, IDs, percentages, or records. "
|
| 919 |
+
"If real attendance data is provided, analyse it thoroughly. "
|
| 920 |
+
"If no records are provided, set overallRate to null and return empty arrays. "
|
| 921 |
+
"Never expose raw database document IDs in any text field. "
|
| 922 |
"Return only JSON."
|
| 923 |
)
|
| 924 |
|
|
|
|
| 929 |
Scope ID: {scope_id or 'N/A'}
|
| 930 |
Date Range: {json.dumps(date_range) if date_range else 'current term'}
|
| 931 |
|
| 932 |
+
ATTENDANCE RECORDS (analyse these β do NOT invent data):
|
| 933 |
+
{json.dumps(records, ensure_ascii=False) if has_records else 'NO RECORDS PROVIDED β return null overallRate and empty arrays.'}
|
| 934 |
+
|
| 935 |
+
STRICT RULES:
|
| 936 |
+
- Derive ALL percentages, patterns, and at-risk flags from the records above only.
|
| 937 |
+
- Do not invent student names or IDs beyond what appears in the records.
|
| 938 |
- Do not fabricate attendance percentages or absence counts.
|
|
|
|
| 939 |
- Never expose raw database document IDs in any text field (summary, detail, recommendation).
|
| 940 |
+
- Calculate overallRate as: (present records / total records) * 100, rounded to 1 decimal.
|
| 941 |
|
| 942 |
Return ONLY valid JSON:
|
| 943 |
{{
|
| 944 |
"scope": "{scope}",
|
| 945 |
"scopeId": "{scope_id or ''}",
|
| 946 |
+
"period": {json.dumps(date_range) if date_range else '{"from": "term start", "to": "term end"}'},
|
| 947 |
+
"overallRate": <calculated percentage or null if no records provided>,
|
| 948 |
"patterns": [
|
| 949 |
{{
|
| 950 |
"type": "<day_of_week|chronic_absence|weather_related|etc>",
|
| 951 |
+
"detail": "<description based only on provided records>",
|
| 952 |
"severity": "<low|medium|high>",
|
| 953 |
+
"studentIds": ["<id from records only>"]
|
| 954 |
}}
|
| 955 |
],
|
| 956 |
"atRiskStudents": [
|
| 957 |
{{
|
| 958 |
+
"studentId": "<id from records only>",
|
| 959 |
+
"studentName": "<name from records only β never invented>",
|
| 960 |
+
"absenceRate": <calculated percentage>,
|
| 961 |
"trend": "<improving|stable|worsening>",
|
| 962 |
"recommendation": "<action>"
|
| 963 |
}}
|
| 964 |
],
|
| 965 |
+
"summary": "<overall summary derived from the provided records only>"
|
| 966 |
}}
|
| 967 |
"""
|
| 968 |
result = gemini_json(prompt, system_instruction=system_instruction)
|
| 969 |
|
| 970 |
+
# Sanitise: pin scope fields to request values
|
| 971 |
if result:
|
| 972 |
result['scope'] = scope
|
| 973 |
result['scopeId'] = scope_id or ''
|