vibe coded frontend

This commit is contained in:
MangoPig
2026-05-12 10:34:42 +01:00
parent 0fdec18a15
commit 675285e99d
99 changed files with 57409 additions and 29 deletions

148
Mock-Data/README.md Normal file
View File

@@ -0,0 +1,148 @@
# Mock Pupil-History Dataset — Learning Path Agent Hackathon
Hand-crafted, internally-consistent JSON dataset for the BoostAI **Learning
Path Agent** hackathon challenge. Mirrors the production SQLAlchemy schema
in `elevenplus-backend/src/app/models/` so an agent built on this data
transfers cleanly to the real database after the hackathon.
**Reference date:** `2026-05-01` (timestamps in the data are relative to
this — adjust `TODAY` in `generate.py` to shift them).
## What's in here
```
classroom.json One Year-6 Maths class + tutor + student-classroom links
students.json 12 students (with _persona annotation per student)
question_bank.json 50 unique 11+ Maths questions
assignments.json 8 assignments (5 CLOSED, 2 PUBLISHED, 1 DRAFT)
assignment_questions.json 64 assignment-question rows (links to question_bank)
assignment_assignees.json 85 (student × assignment) rows with status/scores
student_answers.json 593 per-question answer records — THE primary file
activity_logs.json 593 activity log rows (timestamps, durations, solve_mode)
dataset.json All of the above bundled into one object
generate.py Deterministic generator (re-run anytime)
```
The agent's main input is `dataset.json` (or `student_answers.json` plus
`question_bank.json` if you'd rather load files separately).
## Schema fidelity
Field names and enum values match the production models exactly:
| Production model | This dataset |
|---|---|
| `users.py` (role=student) | `students.json` |
| `classrooms.py` + `classroom_student_rs.py` | `classroom.json` |
| `assignments.py` (status: DRAFT/PUBLISHED/CLOSED) | `assignments.json` |
| `assignment_questions.py` | `assignment_questions.json` |
| `assignment_assignees.py` (status: NOT_STARTED/IN_PROGRESS/SUBMITTED) | `assignment_assignees.json` |
| `question_bank.py` (difficulty: EASY/MEDIUM/HARD; source: BOOST) | `question_bank.json` |
| `student_assignment_answers.py` (graded_marks, marks_awarded, grading_status=GRADED) | `student_answers.json` |
| `assignment_activity_logs.py` (activity_type, duration_seconds, extra_data) | `activity_logs.json` |
| `question_metrics.py` (explanation_type) | embedded as `_solve_mode` on each answer |
Timestamps are **Unix milliseconds** (BigInteger) per the project convention.
### Hackathon annotations (not in production schema)
Fields prefixed with **`_`** are hackathon-only annotations. Strip them if
you ever seed this data into the real DB.
| Annotation | Where | Meaning |
|---|---|---|
| `_persona` | `students.json` | Engineered behaviour persona (see below) |
| `_solve_mode` | `student_answers.json` | One of `just_answer`, `step_by_step`, `solve_together`, `handwritten` |
| `_time_on_task_seconds` | `student_answers.json` | Seconds spent on this question |
| `_is_correct` | `student_answers.json` | Boolean correctness (already implied by `graded_marks`) |
| `_misconception_tag` | `student_answers.json` | Set when wrong answer matches a known misconception (e.g. `add_tops_add_bottoms`) |
| `_question_topic` / `_sub_topic` / `_difficulty` | `student_answers.json` | Denormalised from `question_bank` for convenience |
| `_answered_at` | `student_answers.json` | Same as `created_at`, just clearer name |
## The 12 students
Five students have engineered misconception personas; the other seven are
realistic noise. The agent should ideally identify the personas from the
data itself — `_persona` is included only so you can grade the agent.
| ID | Name | Persona | What you'll see in the data |
|---|---|---|---|
| 201 | Aisha Khan | `fraction_inversion` | ~12% on Fractions Add/Subtract/Multiply, ~78% elsewhere. Wrong answers show **add-tops-add-bottoms** pattern (e.g. ½+⅓ → ⅖). |
| 202 | Ben Carter | `place_value_gaps` | Fails multi-digit subtraction with borrowing & decimal alignment. Strong on single-digit ops. |
| 203 | Chen Wei | `rushed_careless` | Right method when in `step_by_step`; wrong final answer in `just_answer`. Time-on-task drops week-over-week. **No activity in the last 8 days** — also drives Early Warning. |
| 204 | Daniela Rossi | `solve_together_dependent` | Solve-Together share rises **21% → 88%** across the period. Independent accuracy degrading. |
| 205 | Elif Demir | `word_problem_weak` | 0% on word problems, ~90% on bare computation of the same operations. |
| 206 | Felix Brown | `stable_strong` | ~84% overall — baseline noise. |
| 207 | Grace Park | `stable_strong` | ~85% overall — baseline noise. |
| 208210 | Singh / Nakamura / Williams | `stable_mid` | ~65% overall — baseline noise. |
| 211212 | Patel / O'Connor | `stable_weak` | ~50% overall — baseline noise. |
## The 8 assignments
| ID | Name | Topic | Due | Status | Why it matters |
|---|---|---|---|---|---|
| 3001 | HW1 — Place Value Warmup | Place Value | -28d | CLOSED | Baseline data |
| 3002 | HW2 — Arithmetic Practice | Arithmetic | -22d | CLOSED | |
| 3003 | HW3 — Fractions Foundations | Fractions | -16d | CLOSED | First fraction signal |
| 3004 | HW4 — Negatives & BIDMAS | BIDMAS | -10d | CLOSED | |
| 3005 | HW5 — Geometry Basics | Geometry | -6d | CLOSED | |
| 3006 | HW6 — Algebra & Sequences | Algebra | +2d | PUBLISHED | In flight |
| **3007** | **HW7 — Adding Fractions (test prep)** | **Fractions** | **+5d** | **PUBLISHED** | **Curriculum deadline anchor for the bonus EWS** |
| 3008 | HW8 — Mixed Revision | Mixed | +12d | DRAFT | No activity yet |
## Bonus / Early Warning signals embedded in the data
| Signal | Where it lives | Who exhibits it |
|---|---|---|
| Drop in attempt rate (last 7 days) | `student_answers.json` timestamps | Student 203 (no recent activity); 211 reduced volume |
| Increasing Solve-Together reliance | `_solve_mode` distribution over time | Student 204 (21% → 88%) |
| Declining score trend on deadline topic | Fractions accuracy across HW3 → HW7 | Student 201 (very weak on Fractions; HW7 is fractions, due in 5 days) |
| Time since last session | `activity_logs.json` last timestamp | Student 203 (≥ 8 days) |
**Expected agent output for the bonus monitor on this dataset:**
| Rank | Student | Topic driving risk | Suggested action |
|---|---|---|---|
| 1 | 203 Chen Wei | Engagement collapse | Reach out + light re-entry assignment; check for blockers |
| 2 | 201 Aisha Khan | Fractions / Add — HW7 in 5 days | 3 targeted fraction-add questions on the add-tops-add-bottoms misconception |
| 3 | 204 Daniela Rossi | Increasing scaffold dependence (any topic) | Pair with `step_by_step` mode + scheduled `just_answer` checkpoint |
## Running / re-running the generator
```bash
cd boost-ai-eval/mock-data
python3 generate.py
```
The RNG is seeded (`20260501`), so output is byte-stable across runs unless
you change the source. To shift dates, edit `TODAY` at the top of
`generate.py`.
## Quick agent-side recipes
**Load everything:**
```python
import json
data = json.load(open('boost-ai-eval/mock-data/dataset.json'))
students = data['students']
answers = data['student_answers']
qbank = {q['id']: q for q in data['question_bank']}
```
**Pull all answers for a single student:**
```python
def answers_for(student_id):
aa_ids = {aa['id'] for aa in data['assignment_assignees']
if aa['student_id'] == student_id}
return [a for a in data['student_answers'] if a['assignee_id'] in aa_ids]
```
**Compute topic-level mastery snapshot:**
```python
from collections import defaultdict
topic_acc = defaultdict(lambda: [0, 0]) # [correct, total]
for a in answers_for(201):
topic_acc[a['_question_topic']][1] += 1
topic_acc[a['_question_topic']][0] += int(a['_is_correct'])
# {'Fractions': [2, 16], 'Place Value': [3, 4], ...}
```

7646
Mock-Data/activity_logs.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,578 @@
[
{
"id": 300101,
"assignment_id": 3001,
"question_bank_id": 1001,
"question_order": 1,
"maximum_marks": 1,
"rubric": null,
"created_at": 1774605600000
},
{
"id": 300102,
"assignment_id": 3001,
"question_bank_id": 1002,
"question_order": 2,
"maximum_marks": 1,
"rubric": null,
"created_at": 1774605600000
},
{
"id": 300103,
"assignment_id": 3001,
"question_bank_id": 1003,
"question_order": 3,
"maximum_marks": 1,
"rubric": null,
"created_at": 1774605600000
},
{
"id": 300104,
"assignment_id": 3001,
"question_bank_id": 1101,
"question_order": 4,
"maximum_marks": 1,
"rubric": null,
"created_at": 1774605600000
},
{
"id": 300105,
"assignment_id": 3001,
"question_bank_id": 1102,
"question_order": 5,
"maximum_marks": 1,
"rubric": null,
"created_at": 1774605600000
},
{
"id": 300106,
"assignment_id": 3001,
"question_bank_id": 1301,
"question_order": 6,
"maximum_marks": 1,
"rubric": null,
"created_at": 1774605600000
},
{
"id": 300107,
"assignment_id": 3001,
"question_bank_id": 1401,
"question_order": 7,
"maximum_marks": 1,
"rubric": null,
"created_at": 1774605600000
},
{
"id": 300108,
"assignment_id": 3001,
"question_bank_id": 1701,
"question_order": 8,
"maximum_marks": 1,
"rubric": null,
"created_at": 1774605600000
},
{
"id": 300201,
"assignment_id": 3002,
"question_bank_id": 1101,
"question_order": 1,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775124000000
},
{
"id": 300202,
"assignment_id": 3002,
"question_bank_id": 1102,
"question_order": 2,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775124000000
},
{
"id": 300203,
"assignment_id": 3002,
"question_bank_id": 1103,
"question_order": 3,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775124000000
},
{
"id": 300204,
"assignment_id": 3002,
"question_bank_id": 1104,
"question_order": 4,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775124000000
},
{
"id": 300205,
"assignment_id": 3002,
"question_bank_id": 1105,
"question_order": 5,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775124000000
},
{
"id": 300206,
"assignment_id": 3002,
"question_bank_id": 1106,
"question_order": 6,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775124000000
},
{
"id": 300207,
"assignment_id": 3002,
"question_bank_id": 1801,
"question_order": 7,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775124000000
},
{
"id": 300208,
"assignment_id": 3002,
"question_bank_id": 1803,
"question_order": 8,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775124000000
},
{
"id": 300301,
"assignment_id": 3003,
"question_bank_id": 1401,
"question_order": 1,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775642400000
},
{
"id": 300302,
"assignment_id": 3003,
"question_bank_id": 1402,
"question_order": 2,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775642400000
},
{
"id": 300303,
"assignment_id": 3003,
"question_bank_id": 1411,
"question_order": 3,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775642400000
},
{
"id": 300304,
"assignment_id": 3003,
"question_bank_id": 1412,
"question_order": 4,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775642400000
},
{
"id": 300305,
"assignment_id": 3003,
"question_bank_id": 1413,
"question_order": 5,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775642400000
},
{
"id": 300306,
"assignment_id": 3003,
"question_bank_id": 1421,
"question_order": 6,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775642400000
},
{
"id": 300307,
"assignment_id": 3003,
"question_bank_id": 1422,
"question_order": 7,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775642400000
},
{
"id": 300308,
"assignment_id": 3003,
"question_bank_id": 1802,
"question_order": 8,
"maximum_marks": 1,
"rubric": null,
"created_at": 1775642400000
},
{
"id": 300401,
"assignment_id": 3004,
"question_bank_id": 1201,
"question_order": 1,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776160800000
},
{
"id": 300402,
"assignment_id": 3004,
"question_bank_id": 1202,
"question_order": 2,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776160800000
},
{
"id": 300403,
"assignment_id": 3004,
"question_bank_id": 1203,
"question_order": 3,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776160800000
},
{
"id": 300404,
"assignment_id": 3004,
"question_bank_id": 1301,
"question_order": 4,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776160800000
},
{
"id": 300405,
"assignment_id": 3004,
"question_bank_id": 1302,
"question_order": 5,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776160800000
},
{
"id": 300406,
"assignment_id": 3004,
"question_bank_id": 1303,
"question_order": 6,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776160800000
},
{
"id": 300407,
"assignment_id": 3004,
"question_bank_id": 1502,
"question_order": 7,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776160800000
},
{
"id": 300408,
"assignment_id": 3004,
"question_bank_id": 1701,
"question_order": 8,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776160800000
},
{
"id": 300501,
"assignment_id": 3005,
"question_bank_id": 1601,
"question_order": 1,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776506400000
},
{
"id": 300502,
"assignment_id": 3005,
"question_bank_id": 1602,
"question_order": 2,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776506400000
},
{
"id": 300503,
"assignment_id": 3005,
"question_bank_id": 1603,
"question_order": 3,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776506400000
},
{
"id": 300504,
"assignment_id": 3005,
"question_bank_id": 1611,
"question_order": 4,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776506400000
},
{
"id": 300505,
"assignment_id": 3005,
"question_bank_id": 1612,
"question_order": 5,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776506400000
},
{
"id": 300506,
"assignment_id": 3005,
"question_bank_id": 1613,
"question_order": 6,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776506400000
},
{
"id": 300507,
"assignment_id": 3005,
"question_bank_id": 1804,
"question_order": 7,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776506400000
},
{
"id": 300508,
"assignment_id": 3005,
"question_bank_id": 1805,
"question_order": 8,
"maximum_marks": 1,
"rubric": null,
"created_at": 1776506400000
},
{
"id": 300601,
"assignment_id": 3006,
"question_bank_id": 1501,
"question_order": 1,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777197600000
},
{
"id": 300602,
"assignment_id": 3006,
"question_bank_id": 1502,
"question_order": 2,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777197600000
},
{
"id": 300603,
"assignment_id": 3006,
"question_bank_id": 1503,
"question_order": 3,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777197600000
},
{
"id": 300604,
"assignment_id": 3006,
"question_bank_id": 1511,
"question_order": 4,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777197600000
},
{
"id": 300605,
"assignment_id": 3006,
"question_bank_id": 1512,
"question_order": 5,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777197600000
},
{
"id": 300606,
"assignment_id": 3006,
"question_bank_id": 1513,
"question_order": 6,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777197600000
},
{
"id": 300607,
"assignment_id": 3006,
"question_bank_id": 1804,
"question_order": 7,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777197600000
},
{
"id": 300608,
"assignment_id": 3006,
"question_bank_id": 1702,
"question_order": 8,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777197600000
},
{
"id": 300701,
"assignment_id": 3007,
"question_bank_id": 1411,
"question_order": 1,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777456800000
},
{
"id": 300702,
"assignment_id": 3007,
"question_bank_id": 1412,
"question_order": 2,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777456800000
},
{
"id": 300703,
"assignment_id": 3007,
"question_bank_id": 1413,
"question_order": 3,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777456800000
},
{
"id": 300704,
"assignment_id": 3007,
"question_bank_id": 1414,
"question_order": 4,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777456800000
},
{
"id": 300705,
"assignment_id": 3007,
"question_bank_id": 1415,
"question_order": 5,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777456800000
},
{
"id": 300706,
"assignment_id": 3007,
"question_bank_id": 1416,
"question_order": 6,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777456800000
},
{
"id": 300707,
"assignment_id": 3007,
"question_bank_id": 1802,
"question_order": 7,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777456800000
},
{
"id": 300708,
"assignment_id": 3007,
"question_bank_id": 1422,
"question_order": 8,
"maximum_marks": 1,
"rubric": null,
"created_at": 1777456800000
},
{
"id": 300801,
"assignment_id": 3008,
"question_bank_id": 1004,
"question_order": 1,
"maximum_marks": 1,
"rubric": null,
"created_at": 1778061600000
},
{
"id": 300802,
"assignment_id": 3008,
"question_bank_id": 1106,
"question_order": 2,
"maximum_marks": 1,
"rubric": null,
"created_at": 1778061600000
},
{
"id": 300803,
"assignment_id": 3008,
"question_bank_id": 1303,
"question_order": 3,
"maximum_marks": 1,
"rubric": null,
"created_at": 1778061600000
},
{
"id": 300804,
"assignment_id": 3008,
"question_bank_id": 1416,
"question_order": 4,
"maximum_marks": 1,
"rubric": null,
"created_at": 1778061600000
},
{
"id": 300805,
"assignment_id": 3008,
"question_bank_id": 1503,
"question_order": 5,
"maximum_marks": 1,
"rubric": null,
"created_at": 1778061600000
},
{
"id": 300806,
"assignment_id": 3008,
"question_bank_id": 1613,
"question_order": 6,
"maximum_marks": 1,
"rubric": null,
"created_at": 1778061600000
},
{
"id": 300807,
"assignment_id": 3008,
"question_bank_id": 1712,
"question_order": 7,
"maximum_marks": 1,
"rubric": null,
"created_at": 1778061600000
},
{
"id": 300808,
"assignment_id": 3008,
"question_bank_id": 1805,
"question_order": 8,
"maximum_marks": 1,
"rubric": null,
"created_at": 1778061600000
}
]

View File

@@ -0,0 +1,98 @@
[
{
"id": 3001,
"name": "HW1 \u2014 Place Value Warmup",
"teacher_id": 100,
"topic": "Place Value",
"due_date": 1775260740000,
"status": "CLOSED",
"maximum_marks": 8,
"is_deleted": false,
"created_at": 1774605600000,
"updated_at": 1774605600000
},
{
"id": 3002,
"name": "HW2 \u2014 Arithmetic Practice",
"teacher_id": 100,
"topic": "Arithmetic",
"due_date": 1775779140000,
"status": "CLOSED",
"maximum_marks": 8,
"is_deleted": false,
"created_at": 1775124000000,
"updated_at": 1775124000000
},
{
"id": 3003,
"name": "HW3 \u2014 Fractions Foundations",
"teacher_id": 100,
"topic": "Fractions",
"due_date": 1776297540000,
"status": "CLOSED",
"maximum_marks": 8,
"is_deleted": false,
"created_at": 1775642400000,
"updated_at": 1775642400000
},
{
"id": 3004,
"name": "HW4 \u2014 Negatives & BIDMAS",
"teacher_id": 100,
"topic": "BIDMAS",
"due_date": 1776815940000,
"status": "CLOSED",
"maximum_marks": 8,
"is_deleted": false,
"created_at": 1776160800000,
"updated_at": 1776160800000
},
{
"id": 3005,
"name": "HW5 \u2014 Geometry Basics",
"teacher_id": 100,
"topic": "Geometry",
"due_date": 1777161540000,
"status": "CLOSED",
"maximum_marks": 8,
"is_deleted": false,
"created_at": 1776506400000,
"updated_at": 1776506400000
},
{
"id": 3006,
"name": "HW6 \u2014 Algebra & Sequences",
"teacher_id": 100,
"topic": "Algebra",
"due_date": 1777852740000,
"status": "PUBLISHED",
"maximum_marks": 8,
"is_deleted": false,
"created_at": 1777197600000,
"updated_at": 1777197600000
},
{
"id": 3007,
"name": "HW7 \u2014 Adding Fractions (test prep)",
"teacher_id": 100,
"topic": "Fractions",
"due_date": 1778111940000,
"status": "PUBLISHED",
"maximum_marks": 8,
"is_deleted": false,
"created_at": 1777456800000,
"updated_at": 1777456800000
},
{
"id": 3008,
"name": "HW8 \u2014 Mixed Revision",
"teacher_id": 100,
"topic": "Mixed",
"due_date": 1778716740000,
"status": "DRAFT",
"maximum_marks": 8,
"is_deleted": false,
"created_at": 1778061600000,
"updated_at": 1778061600000
}
]

101
Mock-Data/classroom.json Normal file
View File

@@ -0,0 +1,101 @@
{
"classroom": {
"id": 500,
"name": "Year 6 \u2014 Maths Set 1",
"organization_id": 1,
"tutor_id": 100,
"invite_code": "Y6MATHS1",
"target_level": 6,
"archived": false,
"hide_just_answer": false,
"is_deleted": false,
"created_at": 1772445600000,
"updated_at": 1772445600000
},
"tutor": {
"id": 100,
"fullname": "Sarah Johnson",
"email": "sarah.johnson@boostai.example",
"username": "sjohnson",
"role": "tutor",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1762077600000,
"updated_at": 1762077600000
},
"classroom_student_rs": [
{
"id": 801,
"classroom_id": 500,
"student_id": 201,
"created_at": 1772877600000
},
{
"id": 802,
"classroom_id": 500,
"student_id": 202,
"created_at": 1772877600000
},
{
"id": 803,
"classroom_id": 500,
"student_id": 203,
"created_at": 1772877600000
},
{
"id": 804,
"classroom_id": 500,
"student_id": 204,
"created_at": 1772877600000
},
{
"id": 805,
"classroom_id": 500,
"student_id": 205,
"created_at": 1772877600000
},
{
"id": 806,
"classroom_id": 500,
"student_id": 206,
"created_at": 1772877600000
},
{
"id": 807,
"classroom_id": 500,
"student_id": 207,
"created_at": 1772877600000
},
{
"id": 808,
"classroom_id": 500,
"student_id": 208,
"created_at": 1772877600000
},
{
"id": 809,
"classroom_id": 500,
"student_id": 209,
"created_at": 1772877600000
},
{
"id": 810,
"classroom_id": 500,
"student_id": 210,
"created_at": 1772877600000
},
{
"id": 811,
"classroom_id": 500,
"student_id": 211,
"created_at": 1772877600000
},
{
"id": 812,
"classroom_id": 500,
"student_id": 212,
"created_at": 1772877600000
}
]
}

24386
Mock-Data/dataset.json Normal file

File diff suppressed because it is too large Load Diff

830
Mock-Data/generate.py Normal file
View File

@@ -0,0 +1,830 @@
"""
Mock pupil-history dataset generator for the BoostAI "Learning Path Agent"
hackathon challenge.
Outputs JSON files under the same directory, mirroring the production
SQLAlchemy schema in elevenplus-backend/src/app/models/. The generator is
deterministic (seeded RNG) so re-runs produce identical output.
Run:
python3 generate.py
Outputs:
classroom.json, students.json, question_bank.json, assignments.json,
assignment_questions.json, assignment_assignees.json,
student_answers.json, activity_logs.json, dataset.json
"""
from __future__ import annotations
import json
import os
import random
from datetime import datetime, timedelta, timezone
from pathlib import Path
# Reference date — keep in sync with the hackathon brief.
TODAY = datetime(2026, 5, 1, 9, 0, 0, tzinfo=timezone.utc)
OUT_DIR = Path(__file__).parent
RNG = random.Random(20260501)
def ms(dt: datetime) -> int:
return int(dt.timestamp() * 1000)
def days_ago(n: float, hour: int = 10, minute: int = 0) -> datetime:
return (TODAY - timedelta(days=n)).replace(hour=hour, minute=minute, second=0, microsecond=0)
# ---------------------------------------------------------------------------
# Classroom + Tutor + Students
# ---------------------------------------------------------------------------
TUTOR = {
"id": 100,
"fullname": "Sarah Johnson",
"email": "sarah.johnson@boostai.example",
"username": "sjohnson",
"role": "tutor",
"active": True,
"is_test": False,
"is_deleted": False,
"created_at": ms(days_ago(180)),
"updated_at": ms(days_ago(180)),
}
CLASSROOM = {
"id": 500,
"name": "Year 6 — Maths Set 1",
"organization_id": 1,
"tutor_id": TUTOR["id"],
"invite_code": "Y6MATHS1",
"target_level": 6,
"archived": False,
"hide_just_answer": False,
"is_deleted": False,
"created_at": ms(days_ago(60)),
"updated_at": ms(days_ago(60)),
}
# 12 students. _persona is a hackathon annotation (not in the production
# schema) — used to drive answer generation below and to document expected
# agent output. Strip the underscore-prefixed fields if seeding the real DB.
STUDENTS_RAW = [
(201, "Aisha Khan", "fraction_inversion"),
(202, "Ben Carter", "place_value_gaps"),
(203, "Chen Wei", "rushed_careless"),
(204, "Daniela Rossi", "solve_together_dependent"),
(205, "Elif Demir", "word_problem_weak"),
(206, "Felix Brown", "stable_strong"),
(207, "Grace Park", "stable_strong"),
(208, "Harry Singh", "stable_mid"),
(209, "Isla Nakamura", "stable_mid"),
(210, "Jaden Williams", "stable_mid"),
(211, "Kira Patel", "stable_weak"),
(212, "Liam O'Connor", "stable_weak"),
]
STUDENTS = []
CLASSROOM_STUDENT_RS = []
for sid, fullname, persona in STUDENTS_RAW:
first = fullname.split()[0].lower().replace("'", "")
STUDENTS.append({
"id": sid,
"fullname": fullname,
"email": f"{first}.{sid}@boostai.example",
"username": f"{first}{sid}",
"role": "student",
"active": True,
"is_test": False,
"is_deleted": False,
"created_at": ms(days_ago(55)),
"updated_at": ms(days_ago(55)),
"_persona": persona,
})
CLASSROOM_STUDENT_RS.append({
"id": 600 + sid,
"classroom_id": CLASSROOM["id"],
"student_id": sid,
"created_at": ms(days_ago(55)),
})
# ---------------------------------------------------------------------------
# Question Bank — Maths, 11+
# Fields mirror question_bank.py.
# _wrong_answer_map: hackathon helper — for misconception personas, the
# answer they typically produce. Not in production schema.
# ---------------------------------------------------------------------------
QUESTION_BANK = [
# ---- Place Value (tens, hundreds, thousands, decimals) ----
{"id": 1001, "topic": "Place Value", "sub_topic": "Multi-digit numbers", "tag": None,
"difficulty": "EASY",
"question_text": "What is the value of the digit 7 in the number 4,732?",
"correct_answer": "700",
"_wrong_answers": {"place_value_gaps": "70"}},
{"id": 1002, "topic": "Place Value", "sub_topic": "Multi-digit numbers", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Round 24,587 to the nearest thousand.",
"correct_answer": "25000",
"_wrong_answers": {"place_value_gaps": "24000"}},
{"id": 1003, "topic": "Place Value", "sub_topic": "Decimals", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Write 0.07 as a fraction in its simplest form.",
"correct_answer": "7/100",
"_wrong_answers": {"place_value_gaps": "7/10"}},
{"id": 1004, "topic": "Place Value", "sub_topic": "Decimals", "tag": None,
"difficulty": "HARD",
"question_text": "What is 0.3 + 0.07?",
"correct_answer": "0.37",
"_wrong_answers": {"place_value_gaps": "0.10"}},
# ---- Arithmetic ----
{"id": 1101, "topic": "Arithmetic", "sub_topic": "Addition", "tag": None,
"difficulty": "EASY",
"question_text": "Calculate 246 + 137.",
"correct_answer": "383",
"_wrong_answers": {"rushed_careless": "373"}},
{"id": 1102, "topic": "Arithmetic", "sub_topic": "Subtraction", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Calculate 503 - 47.",
"correct_answer": "456",
"_wrong_answers": {"place_value_gaps": "544", "rushed_careless": "466"}},
{"id": 1103, "topic": "Arithmetic", "sub_topic": "Subtraction", "tag": None,
"difficulty": "HARD",
"question_text": "Calculate 4,002 - 1,375.",
"correct_answer": "2627",
"_wrong_answers": {"place_value_gaps": "3737", "rushed_careless": "2617"}},
{"id": 1104, "topic": "Arithmetic", "sub_topic": "Multiplication", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Calculate 28 x 7.",
"correct_answer": "196",
"_wrong_answers": {"rushed_careless": "186"}},
{"id": 1105, "topic": "Arithmetic", "sub_topic": "Division", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Calculate 144 / 6.",
"correct_answer": "24",
"_wrong_answers": {"rushed_careless": "26"}},
{"id": 1106, "topic": "Arithmetic", "sub_topic": "Multiplication", "tag": None,
"difficulty": "HARD",
"question_text": "Calculate 156 x 24.",
"correct_answer": "3744",
"_wrong_answers": {"rushed_careless": "3724", "place_value_gaps": "374"}},
# ---- Negative Numbers ----
{"id": 1201, "topic": "Negative Numbers", "sub_topic": "Addition", "tag": None,
"difficulty": "EASY",
"question_text": "What is -5 + 8?",
"correct_answer": "3"},
{"id": 1202, "topic": "Negative Numbers", "sub_topic": "Subtraction", "tag": None,
"difficulty": "MEDIUM",
"question_text": "What is 4 - 9?",
"correct_answer": "-5"},
{"id": 1203, "topic": "Negative Numbers", "sub_topic": "Mixed", "tag": None,
"difficulty": "HARD",
"question_text": "What is -7 - (-3)?",
"correct_answer": "-4"},
# ---- BIDMAS ----
{"id": 1301, "topic": "BIDMAS", "sub_topic": None, "tag": None,
"difficulty": "EASY",
"question_text": "Calculate 5 + 3 x 2.",
"correct_answer": "11"},
{"id": 1302, "topic": "BIDMAS", "sub_topic": None, "tag": None,
"difficulty": "MEDIUM",
"question_text": "Calculate (8 - 3) x 4 + 2.",
"correct_answer": "22"},
{"id": 1303, "topic": "BIDMAS", "sub_topic": None, "tag": None,
"difficulty": "HARD",
"question_text": "Calculate 24 / (2 + 4) + 3 x 5.",
"correct_answer": "19",
"_wrong_answers": {"rushed_careless": "17"}},
# ---- Fractions: Equivalent ----
{"id": 1401, "topic": "Fractions", "sub_topic": "Equivalent", "tag": None,
"difficulty": "EASY",
"question_text": "Which fraction is equivalent to 2/4?",
"correct_answer": "1/2"},
{"id": 1402, "topic": "Fractions", "sub_topic": "Equivalent", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Simplify 12/18 to its lowest terms.",
"correct_answer": "2/3"},
{"id": 1403, "topic": "Fractions", "sub_topic": "Equivalent", "tag": None,
"difficulty": "HARD",
"question_text": "Which is larger: 3/5 or 5/8? Give your answer.",
"correct_answer": "5/8"},
# ---- Fractions: Add/Subtract (CORE for fraction_inversion persona) ----
{"id": 1411, "topic": "Fractions", "sub_topic": "Add", "tag": None,
"difficulty": "EASY",
"question_text": "What is 1/2 + 1/3?",
"correct_answer": "5/6",
"_wrong_answers": {"fraction_inversion": "2/5"}},
{"id": 1412, "topic": "Fractions", "sub_topic": "Add", "tag": None,
"difficulty": "EASY",
"question_text": "What is 1/4 + 1/2?",
"correct_answer": "3/4",
"_wrong_answers": {"fraction_inversion": "2/6"}},
{"id": 1413, "topic": "Fractions", "sub_topic": "Add", "tag": None,
"difficulty": "MEDIUM",
"question_text": "What is 2/5 + 1/3?",
"correct_answer": "11/15",
"_wrong_answers": {"fraction_inversion": "3/8"}},
{"id": 1414, "topic": "Fractions", "sub_topic": "Add", "tag": None,
"difficulty": "MEDIUM",
"question_text": "What is 3/4 + 1/6?",
"correct_answer": "11/12",
"_wrong_answers": {"fraction_inversion": "4/10"}},
{"id": 1415, "topic": "Fractions", "sub_topic": "Subtract", "tag": None,
"difficulty": "MEDIUM",
"question_text": "What is 5/6 - 1/3?",
"correct_answer": "1/2",
"_wrong_answers": {"fraction_inversion": "4/3"}},
{"id": 1416, "topic": "Fractions", "sub_topic": "Add", "tag": None,
"difficulty": "HARD",
"question_text": "What is 7/12 + 5/8?",
"correct_answer": "29/24",
"_wrong_answers": {"fraction_inversion": "12/20"}},
# ---- Fractions: Multiply ----
{"id": 1421, "topic": "Fractions", "sub_topic": "Multiply", "tag": None,
"difficulty": "EASY",
"question_text": "What is 1/2 x 1/3?",
"correct_answer": "1/6",
"_wrong_answers": {"fraction_inversion": "2/3"}},
{"id": 1422, "topic": "Fractions", "sub_topic": "Multiply", "tag": None,
"difficulty": "MEDIUM",
"question_text": "What is 2/3 x 3/4?",
"correct_answer": "1/2",
"_wrong_answers": {"fraction_inversion": "5/7"}},
{"id": 1423, "topic": "Fractions", "sub_topic": "Multiply", "tag": None,
"difficulty": "HARD",
"question_text": "What is 4/5 of 35?",
"correct_answer": "28",
"_wrong_answers": {"fraction_inversion": "20"}},
# ---- Algebra: Simple Equations ----
{"id": 1501, "topic": "Algebra", "sub_topic": "Simple Equations", "tag": None,
"difficulty": "EASY",
"question_text": "Solve x + 7 = 12.",
"correct_answer": "5"},
{"id": 1502, "topic": "Algebra", "sub_topic": "Simple Equations", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Solve 3x - 4 = 17.",
"correct_answer": "7",
"_wrong_answers": {"rushed_careless": "8"}},
{"id": 1503, "topic": "Algebra", "sub_topic": "Simple Equations", "tag": None,
"difficulty": "HARD",
"question_text": "Solve 2(x + 3) = 18.",
"correct_answer": "6"},
# ---- Algebra: Sequences ----
{"id": 1511, "topic": "Algebra", "sub_topic": "Sequences", "tag": None,
"difficulty": "EASY",
"question_text": "What is the next term: 2, 5, 8, 11, ___ ?",
"correct_answer": "14"},
{"id": 1512, "topic": "Algebra", "sub_topic": "Sequences", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Find the 10th term of the sequence 4, 7, 10, 13, ...",
"correct_answer": "31"},
{"id": 1513, "topic": "Algebra", "sub_topic": "Sequences", "tag": None,
"difficulty": "HARD",
"question_text": "What is the nth term of the sequence 5, 8, 11, 14, ...?",
"correct_answer": "3n+2"},
# ---- Geometry: Area & Perimeter ----
{"id": 1601, "topic": "Geometry", "sub_topic": "Perimeter", "tag": None,
"difficulty": "EASY",
"question_text": "What is the perimeter of a square with side length 7 cm?",
"correct_answer": "28"},
{"id": 1602, "topic": "Geometry", "sub_topic": "Area", "tag": None,
"difficulty": "MEDIUM",
"question_text": "What is the area of a rectangle 8 cm by 5 cm?",
"correct_answer": "40"},
{"id": 1603, "topic": "Geometry", "sub_topic": "Area", "tag": None,
"difficulty": "HARD",
"question_text": "A right-angled triangle has base 6 cm and height 9 cm. What is its area?",
"correct_answer": "27"},
# ---- Geometry: Angles ----
{"id": 1611, "topic": "Geometry", "sub_topic": "Angles", "tag": None,
"difficulty": "EASY",
"question_text": "Two angles on a straight line are 110° and x. What is x?",
"correct_answer": "70"},
{"id": 1612, "topic": "Geometry", "sub_topic": "Angles", "tag": None,
"difficulty": "MEDIUM",
"question_text": "The angles of a triangle are 45°, 60° and x. What is x?",
"correct_answer": "75"},
{"id": 1613, "topic": "Geometry", "sub_topic": "Angles", "tag": None,
"difficulty": "HARD",
"question_text": "What is the sum of interior angles of a hexagon?",
"correct_answer": "720"},
# ---- Data: Mean / Median / Mode ----
{"id": 1701, "topic": "Data", "sub_topic": "Mean", "tag": None,
"difficulty": "EASY",
"question_text": "Find the mean of 4, 6, 8, 10, 12.",
"correct_answer": "8"},
{"id": 1702, "topic": "Data", "sub_topic": "Median", "tag": None,
"difficulty": "MEDIUM",
"question_text": "Find the median of 3, 7, 2, 9, 5.",
"correct_answer": "5"},
{"id": 1703, "topic": "Data", "sub_topic": "Mode", "tag": None,
"difficulty": "EASY",
"question_text": "Find the mode of 2, 3, 3, 5, 7, 7, 7, 8.",
"correct_answer": "7"},
# ---- Data: Probability ----
{"id": 1711, "topic": "Data", "sub_topic": "Probability", "tag": None,
"difficulty": "MEDIUM",
"question_text": "A bag has 3 red and 5 blue marbles. What is the probability of red?",
"correct_answer": "3/8"},
{"id": 1712, "topic": "Data", "sub_topic": "Probability", "tag": None,
"difficulty": "HARD",
"question_text": "A fair die is rolled. What is the probability of an even number greater than 2?",
"correct_answer": "1/3"},
# ---- Word problems (cross-topic) ----
{"id": 1801, "topic": "Arithmetic", "sub_topic": "Word problem", "tag": "word_problem",
"difficulty": "EASY",
"question_text": "Tom has 24 apples. He gives 9 to his friend. How many does he have left?",
"correct_answer": "15"},
{"id": 1802, "topic": "Fractions", "sub_topic": "Word problem", "tag": "word_problem",
"difficulty": "MEDIUM",
"question_text": "A pizza is cut into 8 slices. Sara eats 1/4 and Tom eats 3/8. What fraction is left?",
"correct_answer": "3/8",
"_wrong_answers": {"word_problem_weak": "1/2", "fraction_inversion": "4/12"}},
{"id": 1803, "topic": "Arithmetic", "sub_topic": "Word problem", "tag": "word_problem",
"difficulty": "MEDIUM",
"question_text": "A train ticket costs £8.50. How much do 6 tickets cost?",
"correct_answer": "51",
"_wrong_answers": {"word_problem_weak": "48", "rushed_careless": "50"}},
{"id": 1804, "topic": "Algebra", "sub_topic": "Word problem", "tag": "word_problem",
"difficulty": "HARD",
"question_text": "Three consecutive numbers add up to 72. What is the smallest number?",
"correct_answer": "23",
"_wrong_answers": {"word_problem_weak": "24"}},
{"id": 1805, "topic": "Geometry", "sub_topic": "Word problem", "tag": "word_problem",
"difficulty": "HARD",
"question_text": "A rectangular garden is 12 m long and 4 m shorter than it is long. What is its area?",
"correct_answer": "96",
"_wrong_answers": {"word_problem_weak": "48"}},
]
# Fill in remaining standard fields for every bank entry.
for q in QUESTION_BANK:
q.setdefault("category", "Math")
q.setdefault("year_level", "Year 6")
q.setdefault("source", "BOOST")
q.setdefault("source_description", None)
q.setdefault("teacher_id", TUTOR["id"])
q.setdefault("maximum_marks", 1)
q.setdefault("rubric", None)
q.setdefault("step_by_step_solution", None)
q.setdefault("image_url", None)
q.setdefault("is_deleted", False)
q.setdefault("created_at", ms(days_ago(40)))
q.setdefault("updated_at", ms(days_ago(40)))
q.setdefault("_wrong_answers", {})
# ---------------------------------------------------------------------------
# Assignments + Assignment Questions
#
# 8 assignments. Distribution: 5 CLOSED (past), 2 PUBLISHED (in-flight),
# 1 DRAFT (future). The deadline-pressure assignment is a Fractions/Add
# assignment due in 5 days — drives the bonus Early Warning topic correlation.
# ---------------------------------------------------------------------------
ASSIGNMENT_DEFS = [
# (id, name, focus_topic, due_offset_days, status, question_bank_ids)
(3001, "HW1 — Place Value Warmup", "Place Value", -28, "CLOSED",
[1001, 1002, 1003, 1101, 1102, 1301, 1401, 1701]),
(3002, "HW2 — Arithmetic Practice", "Arithmetic", -22, "CLOSED",
[1101, 1102, 1103, 1104, 1105, 1106, 1801, 1803]),
(3003, "HW3 — Fractions Foundations", "Fractions", -16, "CLOSED",
[1401, 1402, 1411, 1412, 1413, 1421, 1422, 1802]),
(3004, "HW4 — Negatives & BIDMAS", "BIDMAS", -10, "CLOSED",
[1201, 1202, 1203, 1301, 1302, 1303, 1502, 1701]),
(3005, "HW5 — Geometry Basics", "Geometry", -6, "CLOSED",
[1601, 1602, 1603, 1611, 1612, 1613, 1804, 1805]),
(3006, "HW6 — Algebra & Sequences", "Algebra", 2, "PUBLISHED",
[1501, 1502, 1503, 1511, 1512, 1513, 1804, 1702]),
# The deadline-pressure assignment — bonus Early Warning anchors here.
(3007, "HW7 — Adding Fractions (test prep)", "Fractions", 5, "PUBLISHED",
[1411, 1412, 1413, 1414, 1415, 1416, 1802, 1422]),
(3008, "HW8 — Mixed Revision", "Mixed", 12, "DRAFT",
[1004, 1106, 1303, 1416, 1503, 1613, 1712, 1805]),
]
ASSIGNMENTS = []
ASSIGNMENT_QUESTIONS = []
for aid, name, topic, offset, status, qb_ids in ASSIGNMENT_DEFS:
created_offset = max(offset - 7, -45) # created ~1 week before due
ASSIGNMENTS.append({
"id": aid,
"name": name,
"teacher_id": TUTOR["id"],
"topic": topic,
"due_date": ms(days_ago(-offset, hour=23, minute=59)),
"status": status,
"maximum_marks": len(qb_ids),
"is_deleted": False,
"created_at": ms(days_ago(-created_offset)),
"updated_at": ms(days_ago(-created_offset)),
})
for order, qb_id in enumerate(qb_ids, start=1):
ASSIGNMENT_QUESTIONS.append({
"id": aid * 100 + order,
"assignment_id": aid,
"question_bank_id": qb_id,
"question_order": order,
"maximum_marks": 1,
"rubric": None,
"created_at": ms(days_ago(-created_offset)),
})
# ---------------------------------------------------------------------------
# Assignment Assignees (per student × per assignment) + Student Answers
# ---------------------------------------------------------------------------
QB_BY_ID = {q["id"]: q for q in QUESTION_BANK}
AQ_BY_ID = {aq["id"]: aq for aq in ASSIGNMENT_QUESTIONS}
def assignee_status_for(assignment_status: str, persona: str, aid: int) -> str:
if assignment_status == "DRAFT":
return "NOT_STARTED"
if assignment_status == "PUBLISHED":
# Some students have started it, some not.
return "IN_PROGRESS"
return "SUBMITTED"
# --- Persona-driven correctness/solve-mode generation -----------------------
#
# Each persona is a function that, given the assignment, the question, and
# the assignment's "week index" (0 = oldest, higher = more recent), returns:
# (is_correct, solve_mode, time_seconds, answer_text)
# All randomness flows through RNG (seeded), so output is deterministic.
SOLVE_MODES = ["just_answer", "step_by_step", "solve_together", "handwritten"]
def base_time_for_difficulty(d: str) -> int:
return {"EASY": 60, "MEDIUM": 100, "HARD": 160}[d]
def jitter_time(base: int) -> int:
return max(15, int(base + RNG.randint(-25, 35)))
def pick_mode_default(persona: str, week_idx: int) -> str:
# Most students mostly use just_answer; occasionally step_by_step;
# rarely solve_together.
r = RNG.random()
if r < 0.70:
return "just_answer"
if r < 0.90:
return "step_by_step"
if r < 0.97:
return "solve_together"
return "handwritten"
def answer_for_persona(q: dict, persona: str, force_correct: bool) -> tuple[bool, str]:
"""Return (is_correct, answer_text)."""
if force_correct:
return True, q["correct_answer"]
wrong_map = q.get("_wrong_answers", {}) or {}
if persona in wrong_map:
return False, wrong_map[persona]
# Generic wrong answer.
return False, q["correct_answer"] + "?"
def gen_answer(student: dict, assignment: dict, aq: dict, q: dict, week_idx: int, total_weeks: int):
"""Return a student_answers row dict (or None if assignee hasn't attempted it)."""
persona = student["_persona"]
difficulty = q["difficulty"]
base_time = base_time_for_difficulty(difficulty)
is_word = q.get("tag") == "word_problem"
is_fraction_op = q["topic"] == "Fractions" and q["sub_topic"] in ("Add", "Subtract", "Multiply")
is_place_value = q["topic"] == "Place Value" or (q["topic"] == "Arithmetic" and q["sub_topic"] in ("Subtraction", "Multiplication") and difficulty == "HARD")
# Default: stable_mid baseline.
p_correct = 0.65
solve_mode = pick_mode_default(persona, week_idx)
answered_at_offset_days = 0 # set below
misconception_tag = None
if persona == "fraction_inversion":
if is_fraction_op:
# Sharp misconception: very low on fraction ops, declining.
p_correct = max(0.03, 0.20 - 0.03 * week_idx)
misconception_candidate = "add_tops_add_bottoms" if q["sub_topic"] in ("Add", "Subtract") else "fraction_op_confusion"
elif q["topic"] == "Fractions":
# Equivalent fractions etc: still shaky.
p_correct = 0.25
misconception_candidate = "fraction_general_uncertainty"
else:
p_correct = 0.78
misconception_candidate = None
elif persona == "place_value_gaps":
if is_place_value or (q["topic"] == "Place Value"):
p_correct = 0.25
misconception_candidate = "place_value_misalignment"
else:
p_correct = 0.65
misconception_candidate = None
elif persona == "rushed_careless":
# Right method when forced to slow down (step_by_step), wrong when rushed.
# In just_answer: 40% correct. In step_by_step: 90% correct.
# Time-on-task drops over time (rushing more).
# Solve mode mostly just_answer.
r = RNG.random()
solve_mode = "just_answer" if r < 0.85 else "step_by_step"
if solve_mode == "step_by_step":
p_correct = 0.90
else:
p_correct = 0.40
misconception_candidate = "arithmetic_slip"
# Time decays: week 0 = 0.9 * base, latest = 0.4 * base
t_factor = max(0.4, 0.9 - 0.12 * week_idx)
base_time = int(base_time * t_factor)
elif persona == "solve_together_dependent":
# solve_together usage rises sharply over time. Independent
# accuracy is low and degrading — student is leaning on scaffolding
# more and more.
st_prob = 0.08 + 0.18 * week_idx # week 0 ~8%, week 5 ~98%
st_prob = min(0.92, st_prob)
r = RNG.random()
if r < st_prob:
solve_mode = "solve_together"
p_correct = 0.85
else:
solve_mode = "just_answer" if RNG.random() < 0.7 else "step_by_step"
p_correct = max(0.20, 0.55 - 0.06 * week_idx)
misconception_candidate = "scaffolding_dependence"
elif persona == "word_problem_weak":
if is_word:
p_correct = 0.20
misconception_candidate = "word_problem_interpretation"
else:
p_correct = 0.78
misconception_candidate = None
elif persona == "stable_strong":
p_correct = 0.88 if difficulty != "HARD" else 0.78
misconception_candidate = None
elif persona == "stable_mid":
p_correct = 0.65 if difficulty != "HARD" else 0.50
misconception_candidate = None
elif persona == "stable_weak":
p_correct = 0.55 if difficulty != "HARD" else 0.40
misconception_candidate = None
else:
misconception_candidate = None
# Decide attempted-or-not for IN_PROGRESS assignments.
is_correct = RNG.random() < p_correct
is_correct, answer_text = answer_for_persona(q, persona, force_correct=is_correct)
if not is_correct:
misconception_tag = misconception_candidate
return {
"is_correct": is_correct,
"answer_text": answer_text,
"solve_mode": solve_mode,
"time_on_task_seconds": jitter_time(base_time),
"misconception_tag": misconception_tag,
}
# Order assignments oldest -> newest for week_idx threading.
ASSIGNMENT_DEFS_SORTED = sorted(ASSIGNMENT_DEFS, key=lambda a: a[3]) # by due_offset
weeks_total = len(ASSIGNMENT_DEFS_SORTED)
ASSIGNMENT_ASSIGNEES = []
STUDENT_ANSWERS = []
ACTIVITY_LOGS = []
assignee_id_seq = 4000
answer_id_seq = 50000
log_id_seq = 70000
# Define which students "fall behind" on attempts (bonus signal).
LOW_ATTEMPT_RATE_STUDENTS = {203} # Chen Wei: persona=rushed_careless + 8 days no activity
SKIP_RECENT_ASSIGNMENTS_STUDENTS = {203} # student 3 hasn't attempted recent assignments
for week_idx, assignment_def in enumerate(ASSIGNMENT_DEFS_SORTED):
aid, name, topic, offset, status, qb_ids = assignment_def
a_questions = [aq for aq in ASSIGNMENT_QUESTIONS if aq["assignment_id"] == aid]
for student in STUDENTS:
sid = student["id"]
persona = student["_persona"]
# Skip recent assignments for students with low attempt rate
if status != "CLOSED" and sid in SKIP_RECENT_ASSIGNMENTS_STUDENTS:
assignee = {
"id": assignee_id_seq,
"assignment_id": aid,
"student_id": sid,
"classroom_id": CLASSROOM["id"],
"status": "NOT_STARTED",
"started_at": None,
"submitted_at": None,
"total_marks": None,
"is_active": True,
"deactivated_at": None,
"created_at": ms(days_ago(-(offset - 7))),
}
ASSIGNMENT_ASSIGNEES.append(assignee)
assignee_id_seq += 1
continue
# Skip last published assignment for some low-attempt students
if sid in LOW_ATTEMPT_RATE_STUDENTS and status != "CLOSED":
assignee = {
"id": assignee_id_seq,
"assignment_id": aid,
"student_id": sid,
"classroom_id": CLASSROOM["id"],
"status": "NOT_STARTED",
"started_at": None,
"submitted_at": None,
"total_marks": None,
"is_active": True,
"deactivated_at": None,
"created_at": ms(days_ago(-(offset - 7))),
}
ASSIGNMENT_ASSIGNEES.append(assignee)
assignee_id_seq += 1
continue
# In-progress: ~70% have started and answered ~half the questions
if status == "IN_PROGRESS" or status == "PUBLISHED":
started = RNG.random() < 0.85
submitted = False
if not started:
assignee = {
"id": assignee_id_seq,
"assignment_id": aid,
"student_id": sid,
"classroom_id": CLASSROOM["id"],
"status": "NOT_STARTED",
"started_at": None,
"submitted_at": None,
"total_marks": None,
"is_active": True,
"deactivated_at": None,
"created_at": ms(days_ago(-(offset - 7))),
}
ASSIGNMENT_ASSIGNEES.append(assignee)
assignee_id_seq += 1
continue
else:
started = True
submitted = True
# Date the attempts: roughly the day after the assignment was set
attempt_day_offset = max(offset - 5, -42)
started_at = days_ago(-attempt_day_offset, hour=16 + RNG.randint(0, 3), minute=RNG.randint(0, 59))
# For DRAFT, no assignees needed (skip)
if status == "DRAFT":
continue
assignee_id = assignee_id_seq
assignee_id_seq += 1
# Generate answers
total_score = 0
questions_to_answer = a_questions
if status in ("IN_PROGRESS", "PUBLISHED") and not submitted:
# Partial completion
n_done = RNG.randint(max(1, len(a_questions) // 2), len(a_questions))
questions_to_answer = a_questions[:n_done]
running_time_offset = 0
for aq in questions_to_answer:
q = QB_BY_ID[aq["question_bank_id"]]
ans = gen_answer(student, assignment_def, aq, q, week_idx, weeks_total)
answered_at = started_at + timedelta(seconds=running_time_offset + ans["time_on_task_seconds"])
running_time_offset += ans["time_on_task_seconds"] + RNG.randint(5, 30)
STUDENT_ANSWERS.append({
"id": answer_id_seq,
"assignee_id": assignee_id,
"assignment_question_id": aq["id"],
"answer_type": "LATEX",
"answer_latex": ans["answer_text"],
"extracted_answer": ans["answer_text"],
"graded_marks": 1 if ans["is_correct"] else 0,
"marks_awarded": 1 if ans["is_correct"] else 0,
"ai_reasoning": (
"Answer matches expected solution." if ans["is_correct"]
else f"Incorrect; expected {q['correct_answer']}."
),
"grading_status": "GRADED",
"grading_attempts": 1,
"is_active": True,
"created_at": ms(answered_at),
# ---- Hackathon annotations (not in production schema) ----
"_solve_mode": ans["solve_mode"],
"_time_on_task_seconds": ans["time_on_task_seconds"],
"_is_correct": ans["is_correct"],
"_misconception_tag": ans["misconception_tag"],
"_question_topic": q["topic"],
"_question_sub_topic": q["sub_topic"],
"_question_difficulty": q["difficulty"],
"_answered_at": ms(answered_at),
})
answer_id_seq += 1
total_score += (1 if ans["is_correct"] else 0)
# Activity log
ACTIVITY_LOGS.append({
"id": log_id_seq,
"assignee_id": assignee_id,
"assignment_question_id": aq["id"],
"activity_type": "ANSWERED",
"timestamp": ms(answered_at),
"duration_seconds": ans["time_on_task_seconds"],
"extra_data": {"solve_mode": ans["solve_mode"]},
"created_at": ms(answered_at),
"_student_id": sid,
})
log_id_seq += 1
submitted_at = started_at + timedelta(seconds=running_time_offset) if submitted else None
assignee = {
"id": assignee_id,
"assignment_id": aid,
"student_id": sid,
"classroom_id": CLASSROOM["id"],
"status": "SUBMITTED" if submitted else "IN_PROGRESS",
"started_at": ms(started_at),
"submitted_at": ms(submitted_at) if submitted_at else None,
"total_marks": total_score if submitted else None,
"is_active": True,
"deactivated_at": None,
"created_at": ms(days_ago(-(offset - 7))),
}
ASSIGNMENT_ASSIGNEES.append(assignee)
# ---------------------------------------------------------------------------
# Write outputs
# ---------------------------------------------------------------------------
def write_json(name: str, data) -> None:
path = OUT_DIR / name
with path.open("w") as f:
json.dump(data, f, indent=2, default=str)
print(f" wrote {name} ({len(data) if isinstance(data, list) else 'object'} records)")
print("Generating mock dataset...")
write_json("classroom.json", {
"classroom": CLASSROOM,
"tutor": TUTOR,
"classroom_student_rs": CLASSROOM_STUDENT_RS,
})
write_json("students.json", STUDENTS)
write_json("question_bank.json", QUESTION_BANK)
write_json("assignments.json", ASSIGNMENTS)
write_json("assignment_questions.json", ASSIGNMENT_QUESTIONS)
write_json("assignment_assignees.json", ASSIGNMENT_ASSIGNEES)
write_json("student_answers.json", STUDENT_ANSWERS)
write_json("activity_logs.json", ACTIVITY_LOGS)
dataset = {
"_meta": {
"generated_at_utc": TODAY.isoformat(),
"reference_today": TODAY.date().isoformat(),
"schema_source": "elevenplus-backend/src/app/models/",
"subject": "Maths (UK 11+)",
"students": len(STUDENTS),
"assignments": len(ASSIGNMENTS),
"questions_in_bank": len(QUESTION_BANK),
"student_answers": len(STUDENT_ANSWERS),
"expected_top_3_at_risk_student_ids": [201, 203, 204],
},
"classroom": CLASSROOM,
"tutor": TUTOR,
"classroom_student_rs": CLASSROOM_STUDENT_RS,
"students": STUDENTS,
"question_bank": QUESTION_BANK,
"assignments": ASSIGNMENTS,
"assignment_questions": ASSIGNMENT_QUESTIONS,
"assignment_assignees": ASSIGNMENT_ASSIGNEES,
"student_answers": STUDENT_ANSWERS,
"activity_logs": ACTIVITY_LOGS,
}
write_json("dataset.json", dataset)
print("Done.")

1157
Mock-Data/question_bank.json Normal file

File diff suppressed because it is too large Load Diff

13526
Mock-Data/student_answers.json Normal file

File diff suppressed because it is too large Load Diff

158
Mock-Data/students.json Normal file
View File

@@ -0,0 +1,158 @@
[
{
"id": 201,
"fullname": "Aisha Khan",
"email": "aisha.201@boostai.example",
"username": "aisha201",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "fraction_inversion"
},
{
"id": 202,
"fullname": "Ben Carter",
"email": "ben.202@boostai.example",
"username": "ben202",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "place_value_gaps"
},
{
"id": 203,
"fullname": "Chen Wei",
"email": "chen.203@boostai.example",
"username": "chen203",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "rushed_careless"
},
{
"id": 204,
"fullname": "Daniela Rossi",
"email": "daniela.204@boostai.example",
"username": "daniela204",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "solve_together_dependent"
},
{
"id": 205,
"fullname": "Elif Demir",
"email": "elif.205@boostai.example",
"username": "elif205",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "word_problem_weak"
},
{
"id": 206,
"fullname": "Felix Brown",
"email": "felix.206@boostai.example",
"username": "felix206",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "stable_strong"
},
{
"id": 207,
"fullname": "Grace Park",
"email": "grace.207@boostai.example",
"username": "grace207",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "stable_strong"
},
{
"id": 208,
"fullname": "Harry Singh",
"email": "harry.208@boostai.example",
"username": "harry208",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "stable_mid"
},
{
"id": 209,
"fullname": "Isla Nakamura",
"email": "isla.209@boostai.example",
"username": "isla209",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "stable_mid"
},
{
"id": 210,
"fullname": "Jaden Williams",
"email": "jaden.210@boostai.example",
"username": "jaden210",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "stable_mid"
},
{
"id": 211,
"fullname": "Kira Patel",
"email": "kira.211@boostai.example",
"username": "kira211",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "stable_weak"
},
{
"id": 212,
"fullname": "Liam O'Connor",
"email": "liam.212@boostai.example",
"username": "liam212",
"role": "student",
"active": true,
"is_test": false,
"is_deleted": false,
"created_at": 1772877600000,
"updated_at": 1772877600000,
"_persona": "stable_weak"
}
]