Hostos Academic Learning Center | EdTech

Socratic Math Tutor

The Socratic Math Tutor (Anthony’s Math Tutor) reimagines how AI can support math learning. Instead of giving students instant solutions, it leads them through structured reasoning—mirroring the deliberate questioning of a skilled human tutor.

Dialogue Over Delivery

The tutor follows a simple but powerful rhythm: Explain → Ask → Wait.
Each step provides a brief definition and one focused check question. It does not advance until the learner answers correctly. Wrong answers trigger smaller hints and the same question again, keeping attention on the specific misconception before moving forward. This ensures that progress reflects understanding, not guesswork.

Built-In Reflection

A key feature—the hard reveal policy—requires students to complete at least two verified reasoning steps before they can request the final answer. Even then, they must type “reveal answer” explicitly. The design encourages metacognition: learners pause, think, and justify before closure. It transforms AI tutoring from an answer-delivery system into a continuous formative assessment loop.

Customizable and Curriculum-Aligned

The framework is modular. Educators can add “topic plugins” to define the micro-steps of their own lessons—factoring, solving equations, graphing, and more—while the Socratic logic remains consistent. This gives instructors control over both the content and the cognitive pacing of each tutoring exchange.

Responsible AI in Action

For students, the tool models productive struggle and clear reasoning.
For tutors and instructors, it provides a replicable questioning method that reinforces mathematical thinking.
For institutions, it exemplifies responsible AI use—supporting learning integrity, equity, and transparency.

The Socratic Math Tutor demonstrates how structure and restraint can make AI an ally in genuine learning, not a shortcut around it.

Developed by the Hostos HALC & EdTech teams under the ADELANTE project, which advances ethical, accessible, and student-centered integration of AI in STEM tutoring.

Not a part of CUNY! Copy and paste the HTH system prompt into your LLM!

Under the Hood: System Prompt


/* ---------- SYSTEM PROMPT (load as system/role) ---------- */
export const SYSTEM_PROMPT = `
You are a Socratic university-level math tutor.

Rules:
1) Start in Socratic Mode; first replies for a new problem ask 3–4 short diagnostic questions.
2) Do NOT give final answers or full worked solutions in your first THREE replies.
3) Each turn uses Explain→Ask:
   - Give a brief (1–2 sentences) definition tied to the current step,
   - then exactly ONE focused check question,
   - Do NOT proceed until the learner’s answer to that check is correct.
4) If the answer is incorrect or missing: point out the FIRST issue, give a smaller hint or 1–2 choices, and ask the SAME check again. Do NOT move on.
5) If correct: acknowledge briefly, advance ONE micro-step, ask the next focused question.
6) Withhold exact numeric/closed-form results unless the learner opts out via the Reveal Policy.
7) Reveal Policy:
   - Only if the learner types EXACTLY "reveal answer" or "final answer" (case-insensitive),
   - Only after ≥2 correct micro-checks on this problem,
   - Only when there is NO unanswered check pending,
   - When revealing: concise method outline, then single line "Final Answer:".
8) If this appears graded, never give full solutions; provide guidance and checks only.
`;

/* ----------------- CONFIG (tweak here) ------------------ */
export const CFG = {
  REVEAL_KEYS: ["reveal answer", "final answer"],
  REQ_CORRECT_CHECKS: 2,
  BLOCK_FULL_SOLUTION_FIRST_N_TURNS: 3,
  MAX_DEF_SENTENCES: 2,
  DIRECT_REQUEST_PENALTY: 1, // adds extra required correct checks after a direct-answer request
  directRefusal(defn: string, q: string) {
    return `I can reveal if you type "reveal answer" later. Quick concept: ${defn} — ${q}`;
  },
  gradedRefusal:
    "This seems like graded work. I won’t provide a full solution, but I’ll guide you step by step. Let’s lock this step:",
  incorrect(issue: string) {
    return `Not quite — ${issue}. Here’s a smaller hint:`;
  },
};

/* -------------------- STATE -------------------- */
export type SocraticState = {
  problemId: string | null;
  turnCount: number;
  correctChecks: number;        // confirmed correct checks this problem
  extraRequired: number;        // penalty added by direct-answer requests
  pending?: { q: string; defn: string; validator: (u:string)=>boolean; tips?: string[]; attempts: number };
  graded: boolean;
};

export function initState(): SocraticState {
  return { problemId: null, turnCount: 0, correctChecks: 0, extraRequired: 0, graded: false };
}

/* -------------------- UTILS -------------------- */
const norm = (s:string)=>s.replace(/\s+/g," ").trim().toLowerCase();
const isReveal = (t:string, keys:string[]) => keys.some(k=>norm(t)===norm(k));
const looksGraded = (t:string)=>/\b(deadline|due|submit|graded|quiz|exam|test|assignment|homework|hw|points)\b/i.test(t);
export const V = {
  equalsAny: (arr:string[]) => (u:string)=> arr.map(norm).includes(norm(u)),
  regex: (re:RegExp)=> (u:string)=> re.test(u),
};

/* -------------- MICRO-STEP INTERFACE -------------- */
export type MicroStep = { defn: string; q: string; validator:(u:string)=>boolean; tips?:string[] };
export interface TopicPlugin {
  nextStep(state:SocraticState, userText:string): MicroStep;
  diagnoseIntro?(state:SocraticState): MicroStep[];
}

/* Demo plugin; replace with your curriculum */
export const DemoPlugin: TopicPlugin = {
  nextStep() {
    return {
      defn: "Factoring extracts a common factor so a sum becomes a product, simplifying equations.",
      q: "What is the greatest common factor of 2x^2 and 6x?",
      validator: V.equalsAny(["2x","2·x","2*x"]),
      tips: ["Check both the coefficient and the power of x.", "Does your factor divide BOTH terms completely?"],
    };
  },
};

/* -------------------- HANDLER -------------------- */
export type HandlerInput = { userText:string; state:SocraticState; plugin:TopicPlugin; problemId:string };
export type HandlerOutput = { botText:string; state:SocraticState };

export function handleTurn(inp: HandlerInput): HandlerOutput {
  let { userText, state, plugin, problemId } = inp;
  const msg = userText.trim();

  // Bind/switch problem
  if (state.problemId !== problemId) state = { ...initState(), problemId };

  // Heuristic graded flag
  if (looksGraded(msg)) state.graded = true;

  // If a check is pending, DO NOT allow reveal or advancement until answered correctly
  const hasUnansweredCheck = !!state.pending;

  // Block reveal if any pending check exists (hard lock)
  if (isReveal(msg, CFG.REVEAL_KEYS)) {
    // penalize: repeated direct requests make reveal harder later
    state.extraRequired += CFG.DIRECT_REQUEST_PENALTY;

    if (hasUnansweredCheck) {
      // Re-ask SAME check; no advancement, no reveal
      const p = state.pending!;
      return {
        state,
        botText: CFG.directRefusal(p.defn, p.q),
      };
    }
    // No pending → evaluate gates
    if (state.turnCount < CFG.BLOCK_FULL_SOLUTION_FIRST_N_TURNS) {
      const defn = "We only reveal after a couple of confirmed checks.";
      const q = "Which strategy fits best here?";
      return { state, botText: CFG.directRefusal(defn, q) };
    }
    const need = CFG.REQ_CORRECT_CHECKS + state.extraRequired - state.correctChecks;
    if (need > 0) {
      const defn = "Confirm a few checks to ensure understanding first.";
      const q = "Ready to answer the current step?";
      return { state, botText: `Almost there—answer ${need} more check${need===1?"":"s"}.\n` + CFG.directRefusal(defn, q) };
    }
    if (state.graded) return { state, botText: CFG.gradedRefusal };
    // UI layer should now return method outline + "Final Answer:" line.
    return { state, botText: "Reveal allowed. Provide concise method outline, then one line: “Final Answer: …”." };
  }

  // If a check is pending, validate it first; ANY other message is treated as an attempt
  if (state.pending) {
    const p = state.pending;
    const ok = p.validator(msg);
    if (!ok) {
      p.attempts += 1;
      const tip = p.tips?.[Math.min(p.attempts-1, (p.tips?.length||1)-1)] ?? "Focus on a factor common to BOTH terms.";
      const fb = CFG.incorrect("the factor you chose doesn’t divide both terms evenly");
      return { state, botText: `${fb} ${tip}\nTry again: ${p.q}` };
    }
    // Correct → advance one micro-step only
    state.correctChecks += 1;
    state.pending = undefined;
  }

  // Produce next Explain→Ask micro-step
  const step = plugin.nextStep(state, msg);
  // Trim to 1–2 sentences
  const defn = step.defn.split(/(?<=[.?!])\s+/).slice(0, CFG.MAX_DEF_SENTENCES).join(" ").trim();
  state.turnCount += 1;
  state.pending = { q: step.q, defn, validator: step.validator, tips: step.tips, attempts: 0 };

  // No full solutions in first N replies is enforced by micro-step structure
  return { state, botText: `${defn} ${step.q}` };
}


Leave a comment

Your email address will not be published. Required fields are marked *