Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/vruizz22/innova-backend-serverless/llms.txt

Use this file to discover all available pages before exploring further.

The practice recommender selects the next exercise that maximises diagnostic value for each student’s current ability level, using Item Response Theory 2-Parameter Logistic (2PL) and Fisher information. Rather than cycling through exercises in a fixed order, the engine finds the single item that is most informative given what BKT already knows about the student — the exercise that is neither too easy nor too hard, sitting right at the boundary of the student’s competence.

IRT 2PL Model

Item Response Theory models the probability that a student with ability θ answers a given item correctly. The 2PL model gives each exercise two parameters:
  • a (irtA) — discrimination: how sharply the item distinguishes students just above and just below its difficulty. Higher a means a steeper sigmoid curve.
  • b (irtB) — difficulty: the ability level at which a student has a 50% chance of answering correctly. Positive b means a harder item.
The probability of a correct response is:
P(correct | θ) = 1 / (1 + exp(-a · (θ - b)))
New exercises are initialised with irtA = 1.0 and irtB = 0.0 (average discrimination, average difficulty) and are refined nightly as response data accumulates.

Fisher Information

Fisher information I(θ) quantifies how much an item at difficulty b reduces uncertainty about a student at ability θ. The 2PL Fisher information formula is:
I(θ) = a² · P(θ) · (1 - P(θ))
P(θ) · (1 - P(θ)) is maximised when P(θ) = 0.5, i.e. when the item is exactly at the student’s ability level. The recommender therefore targets items where the student has roughly a 50% chance of success — challenging enough to be diagnostic, accessible enough not to be demoralising. The PracticeService implements this directly:
// practice.service.ts — Fisher information helpers
// IRT 2-PL Fisher information: I(θ) = a² · P(θ) · (1 − P(θ))
function fisherInformation(a: number, b: number, theta: number): number {
  const p = 1.0 / (1.0 + Math.exp(-a * (theta - b)));
  return a * a * p * (1.0 - p);
}

// Logit transform of pKnown (clamped to avoid ±∞).
function thetaFromPKnown(pKnown: number): number {
  const c = Math.min(Math.max(pKnown, 0.05), 0.95);
  return Math.log(c / (1.0 - c));
}
The inner recommendation loop scores every active exercise for the student and returns the one with the highest I(θ):
// practice.service.ts — recommendNext (core loop excerpt)
for (const ex of exercises) {
  const pKnown = masteryByTopic.get(ex.topicId) ?? 0.3;
  const theta = thetaFromPKnown(pKnown);
  const info = fisherInformation(ex.irtA, ex.irtB, theta);
  if (info > bestInfo) {
    bestInfo = info;
    bestExercise = ex;
    bestTheta = theta;
  }
}

θ Estimation from BKT

The student’s ability estimate θ is derived from BKT’s pKnown via the logit transform:
θ = logit(pKnown) = log(pKnown / (1 - pKnown))
pKnown is clamped to [0.05, 0.95] before the transform to avoid ±∞. The mapping is intuitive:
pKnownθ (approx.)Interpretation
0.05−2.94Very low mastery
0.30−0.85BKT prior (no data yet)
0.500.00Average mastery
0.75+1.10High mastery
0.95+2.94Near mastery ceiling
A student with no mastery record for a topic defaults to pKnown = 0.30 (the BKT prior), which maps to θ ≈ −0.85 — steering the recommender toward slightly-below-average difficulty items, appropriate for an unobserved topic.

API Endpoints

Recommend next exercise

GET /practice/recommend-next?studentId=...&domainId=...Returns the single highest-Fisher-information exercise for the student. The optional domainId scopes the search to one domain’s exercises. The response includes the exercise id, problem text, irtA, irtB, student theta, and a human-readable reasoning string.

Course-scoped recommendation

GET /mastery/recommend/:courseId/:studentIdFinds the student’s weakest topic (lowest pKnown) within the course, converts to θ, and returns the best-fit exercise — excluding exercises the student answered correctly in the past 7 days.

Manual assignment

POST /practice/assign with body { studentId, itemIds, dueAt }Creates an assignment from a teacher-specified list of exercise IDs. IRT parameters are not used here — the teacher has already selected the items.

Nightly Calibration

IRT parameters (irtA, irtB) start at default values and improve over time. The innova-ai-engine service runs a nightly calibration job that:
  1. Aggregates all Attempt records for each exercise.
  2. Fits new a and b values using a 2PL marginal maximum likelihood estimator.
  3. Writes the updated values back to the Exercise table in Postgres.
On the next recommendNext call, the updated parameters are used automatically. Topics or exercises with fewer than ~30 responses are skipped and retain their defaults until sufficient data accumulates.

Assignment Creation

Teachers can create assignments in two ways:
  • Recommendation-driven — call GET /practice/recommend-next?studentId=... to retrieve the exercise with maximum Fisher information, then pass the returned exercise ID to POST /practice/assign. The IRT-selected exercise is ready to assign immediately.
  • Manual with IRT context — use POST /practice/assign with a { studentId, itemIds, dueAt } body to create an assignment from a hand-picked list. The IRT parameters on those exercises are still used for future recommendations.
Either path creates an assignment record with the appropriate target rows for the student.
BKT and IRT are complementary models, not alternatives. BKT tracks whether the student knows a topic (the hidden state pKnown). IRT describes how informative a specific item is at a specific ability level. The practice recommender uses BKT output (pKnown → θ) as the input to the IRT item selector (I(θ)), combining the two into a single coherent adaptive loop.

Build docs developers (and LLMs) love