rairo commited on
Commit
d790f9f
Β·
1 Parent(s): ac15bfc

fix: pass client-supplied records/performanceData to Gemini in attendance-analysis and student-insights endpoints

Browse files
Files changed (1) hide show
  1. main.py +64 -35
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 only data explicitly provided in this prompt. "
333
- "In the summary field, refer to the student by their studentId only β€” never invent a name. "
334
- "In subjectBreakdown, only include subjects explicitly listed in the subjects filter; "
335
- "if no filter is given, state that subject-level data was not provided."
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 β€” do not invent subject data'}
343
  Term: {term_id}
344
  Include recommendations: {include_recs}
345
 
346
- IMPORTANT RULES:
347
- - Use the studentId "{student_id}" exactly as provided. Do not invent a student name.
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
- Return ONLY valid JSON matching this exact structure:
 
 
 
 
 
 
353
  {{
354
  "studentId": "{student_id}",
355
- "summary": "<2-3 sentence overview referring to student by ID, not an invented name>",
356
  "subjectBreakdown": [
357
  {{
358
- "subject": "<subject from the provided filter only>",
359
  "trend": "<improving|stable|declining|unknown>",
360
- "averageScore": <number 0-100 or null if not available>,
361
- "insight": "<specific insight based only on provided data>",
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: ensure studentId in output matches what was requested,
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. Never invent, fabricate, or "
892
- "hallucinate student names, student IDs, percentages, or attendance records. "
893
- "Only use data explicitly provided in this prompt. "
894
- "If real attendance data is not provided, set overallRate to null, return empty "
895
- "arrays for patterns and atRiskStudents, and explain in the summary that no data was supplied. "
896
- "Never expose raw database document IDs in summary, detail, or recommendation text fields. "
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
- IMPORTANT RULES:
908
- - Do not invent student names or IDs. Only use names/IDs explicitly provided above.
 
 
 
 
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 '{{"from": "term start", "to": "term end"}}'},
918
- "overallRate": <percentage or null if no data provided>,
919
  "patterns": [
920
  {{
921
  "type": "<day_of_week|chronic_absence|weather_related|etc>",
922
- "detail": "<description based only on provided data>",
923
  "severity": "<low|medium|high>",
924
- "studentIds": ["<id β€” only if explicitly provided>"]
925
  }}
926
  ],
927
  "atRiskStudents": [
928
  {{
929
- "studentId": "<id β€” only if explicitly provided>",
930
- "studentName": "<name β€” only if explicitly provided, never invented>",
931
- "absenceRate": <percentage β€” only if provided>,
932
  "trend": "<improving|stable|worsening>",
933
  "recommendation": "<action>"
934
  }}
935
  ],
936
- "summary": "<overall summary based only on provided data, no invented names or IDs>"
937
  }}
938
  """
939
  result = gemini_json(prompt, system_instruction=system_instruction)
940
 
941
- # Sanitise: pin scope fields to request values so they cannot be hallucinated
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 ''