rpmjp/portfolio
rpmjp/projects/skillbridge/tutor_prompts.py
CompletedFebruary – April 2026

SkillBridge AI — Test Prep Academy Platform

Multi-tenant AI learning platform for test prep academies. Socratic AI tutor that refuses to give answers, AI quiz generation from uploaded files, async pre-grading, weak-spot detection, and AI parent summaries. Full role-based platform across admin, instructor, student, and parent.

LIVE DEMO
Python 3.12FastAPIPostgreSQL 16SQLAlchemy 2.0ReactTypeScriptTailwindGroq / Llama
Languages
Python54.2%
TypeScript45.1%
CSS0.3%
JavaScript0.2%
Dockerfile0.1%
Mako0.1%
tutor_prompts.py
"""
System prompts for each TutorMode. The system prompt is THE most important
part of the tutor — it's what makes the AI refuse to give answers.

This file is where the tutor's pedagogy lives. The base rules forbid giving
answers; each mode layers on a distinct teaching behavior; and build_system_prompt
composes the whole thing with real assignment context so redirection is
specific to what the student is actually working on.
"""

from app.models.assignment import Assignment, TutorMode


_BASE_RULES = """\
You are a Socratic tutor for a test prep academy. Your job is to help the
student LEARN, not to give them answers.

Hard rules you must follow:
- NEVER give the final numerical or text answer to a problem the student is working on.
- If asked directly for an answer ("what is x", "just tell me", "I give up"),
  do NOT comply. Instead, redirect with a question or a hint.
- Keep your responses focused, encouraging, and brief — one idea at a time.
- Always end with a question or a clear next step the student can act on.
- If the student is frustrated, acknowledge it and offer a smaller step,
  but still do not give the answer.
- If the student has clearly tried hard and is genuinely stuck after 3+ exchanges,
  suggest they ask their instructor — never bypass that by giving the answer.
"""


_HINT_MODE = """\
Mode: HINT.

Behavior:
- Wait until the student tells you what they have tried.
- If they have tried something, give ONE small hint that points them in the right direction.
- Do not break the problem into steps. Do not solve.
- Ask one question per response.
"""


_SCAFFOLD_MODE = """\
Mode: SCAFFOLD.

Behavior:
- Break the problem into small steps.
- Walk through one step at a time. Ask the student to do each step themselves.
- After they answer a step, confirm or correct gently, then move to the next step.
- The final step belongs to the student. You may guide it but you do not state the answer.
"""


_CHECK_MODE = """\
Mode: CHECK.

Behavior:
- The student is showing you work in progress. Read it carefully.
- Identify ONE specific place where their reasoning broke (or confirm if it didn't).
- Quote the exact step that needs attention.
- Ask a question that helps them see what to fix.
- Do not rewrite their work for them. Do not give the answer.
"""


_EXPLAIN_MODE = """\
Mode: EXPLAIN (post-submission review).

Behavior:
- The student has already submitted. You may now walk through the full solution
  for learning purposes.
- Show the reasoning step-by-step in plain language.
- Highlight common mistakes and why the correct approach works.
- Connect the technique to similar problems they might see again.
"""


def build_system_prompt(
    assignment: Assignment,
    instructor_materials: list[dict] | None = None,
    student_draft: str | None = None,
    student_uploaded_files: list[str] | None = None,
) -> str:
    """
    Compose the full system prompt for a tutor session.

    The prompt is base rules + the mode's behavior + assignment context
    (title, instructions, rubric criteria) + any instructor-uploaded
    materials + the student's current draft. That context is what lets the
    tutor reference "problem 1" specifically instead of talking in generalities.

    Returns an empty string if mode is DISABLED — the service refuses the
    call before reaching the AI.
    """
    mode_instructions = {
        TutorMode.HINT: _HINT_MODE,
        TutorMode.SCAFFOLD: _SCAFFOLD_MODE,
        TutorMode.CHECK: _CHECK_MODE,
        TutorMode.EXPLAIN: _EXPLAIN_MODE,
    }.get(assignment.tutor_mode, "")

    if not mode_instructions:
        return ""

    parts: list[str] = [_BASE_RULES, mode_instructions]

    # Core assignment context
    assignment_lines = [
        "Assignment context:",
        f"- Title: {assignment.title}",
        f"- Maximum points: {assignment.max_points}",
    ]
    if assignment.due_at:
        assignment_lines.append(f"- Due: {assignment.due_at.isoformat()}")
    assignment_lines.append(f"- Instructions to student:\n{assignment.instructions}")

    # Rubric criteria (the actual descriptors matter, not just totals)
    rubric = assignment.rubric or {}
    criteria = rubric.get("criteria") if isinstance(rubric, dict) else None
    if criteria:
        rubric_lines = ["", "Rubric the work will be graded against:"]
        for c in criteria:
            name = c.get("name", "?")
            pts = c.get("max_points", "?")
            desc = c.get("description", "")
            rubric_lines.append(f"  - {name} ({pts} pts): {desc}")
        assignment_lines.append("\n".join(rubric_lines))

    parts.append("\n".join(assignment_lines))

    # Instructor-uploaded materials (worksheet text the AI should know about).
    # Truncated aggressively — even one PDF can blow the context window.
    if instructor_materials:
        material_lines = ["Instructor-provided materials for this assignment:"]
        for m in instructor_materials:
            filename = m.get("filename", "(unnamed)")
            text = m.get("extracted_text", "")
            if text:
                snippet = text.strip()
                if len(snippet) > 4000:
                    snippet = snippet[:4000] + "... [truncated]"
                material_lines.append(
                    f"\n--- {filename} ---\n{snippet}\n--- end of {filename} ---"
                )
            else:
                material_lines.append(
                    f"- {filename} (couldn't extract text — likely an image or scanned PDF)"
                )
        parts.append("\n".join(material_lines))

    # The student's current draft submission, so the AI can reference their work
    if student_draft and student_draft.strip():
        parts.append(
            f"The student's current draft submission (their work in progress):\n{student_draft.strip()}"
        )

    # Student-uploaded files in the chat (image content sent separately via vision API)
    if student_uploaded_files:
        names = ", ".join(student_uploaded_files)
        parts.append(
            f"The student has attached the following files in this chat: {names}\n"
            "If the file is an image and you can see it, reference it specifically."
        )

    return "\n\n".join(parts)


# Phrases we detect to flag "trying to extract the answer" attempts. The system
# prompt is the primary defense; this detector is a backstop the service can
# use to handle bypass attempts deliberately.
_REDIRECT_TRIGGERS = [
    "just tell me the answer",
    "give me the answer",
    "what's the answer",
    "what is the answer",
    "tell me the answer",
    "just give me",
    "i give up",
    "stop teaching me",
    "skip the lesson",
]


def detect_redirect_attempt(message: str) -> bool:
    """Return True if the student is asking the AI to bypass Socratic teaching."""
    lower = message.lower()
    return any(trigger in lower for trigger in _REDIRECT_TRIGGERS)