We are going to build a lightweight Socratic tutor that adapts its difficulty to a student in real time. It maintains a running profile of mastered and struggling concepts, then adjusts every explanation accordingly. Because we are running it on Oxlo.ai, stuffing a long system prompt and a growing conversation history into every request does not inflate the cost, which makes iterative tutoring actually economical.
What you'll need
- Python 3.10 or newer
pip install openai- An Oxlo.ai API key from https://portal.oxlo.ai
- A topic to study, for example high school physics
Step 1: Bootstrap the client and state
We need an OpenAI-compatible client pointed at Oxlo.ai and a small dataclass to hold the student's evolving profile. I keep the profile minimal so we can inject it directly into the system prompt every turn.
from dataclasses import dataclass, field
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
@dataclass
class StudentProfile:
subject: str = "physics"
level: int = 3 # 1 to 5
mastered: list[str] = field(default_factory=list)
struggling: list[str] = field(default_factory=list)
Step 2: Craft the adaptive system prompt
This template is the actual tutor. By rendering the student's profile into it on every turn, we give the model fresh context without any external vector database.
SYSTEM_PROMPT_TEMPLATE = """You are a patient Socratic tutor helping a student learn {subject}.
The student is currently at level {level} out of 5.
Concepts they have mastered: {mastered}.
Concepts they are struggling with: {struggling}.
Rules:
- Never give the answer directly. Guide the student through hints and questions.
- If the student is stuck on a struggling concept, break the problem into smaller steps.
- If the student shows mastery, increase subtlety and depth.
- Keep responses concise, under 150 words.
- Always end with one concrete question for the student to answer."""
def build_system_prompt(profile: StudentProfile) -> str:
return SYSTEM_PROMPT_TEMPLATE.format(
subject=profile.subject,
level=profile.level,
mastered=", ".join(profile.mastered) or "none yet",
struggling=", ".join(profile.struggling) or "none yet",
)
Step 3: Implement the tutoring turn
Each turn rebuilds the system prompt from the latest profile, appends the student's message, and streams the response back. Streaming matters for a tutor so the student is not staring at a blank screen, and because Oxlo.ai serves Qwen 3 32B with no cold starts the first token arrives quickly.
def tutoring_turn(messages: list[dict], profile: StudentProfile, user_input: str) -> str:
system_prompt = build_system_prompt(profile)
if messages and messages[0]["role"] == "system":
messages[0] = {"role": "system", "content": system_prompt}
else:
messages.insert(0, {"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": user_input})
response = client.chat.completions.create(
model="qwen-3-32b",
messages=messages,
stream=True,
temperature=0.7,
)
reply_chunks = []
for chunk in response:
if chunk.choices[0].delta.content:
text = chunk.choices[0].delta.content
reply_chunks.append(text)
print(text, end="", flush=True)
print()
reply = "".join(reply_chunks)
messages.append({"role": "assistant", "content": reply})
return reply
Step 4: Close the feedback loop
After the student answers, we run a second lightweight call to evaluate whether they demonstrated mastery or confusion. I use DeepSeek V3.2 here because it handles reasoning and coding tasks efficiently, and it sits on Oxlo.ai's free tier so the profile update is gentle on budget. The evaluator returns structured JSON we can merge back into the profile.
import json
def update_profile(profile: StudentProfile, messages: list[dict]) -> StudentProfile:
snippet = json.dumps(messages[-4:], indent=2)
eval_prompt = (
"You are an assessment engine. Given the recent tutor-student dialogue, "
"output ONLY a JSON object with keys: level (int 1-5), mastered (list), "
"struggling (list). Be conservative. Do not hallucinate concepts.\n\n"
f"Dialogue:\n{snippet}"
)
eval_response = client.chat.completions.create(
model="deepseek-v3.2",
messages=[
{"role": "system", "content": "You are an assessment engine that outputs only JSON."},
{"role": "user", "content": eval_prompt},
],
response_format={"type": "json_object"},
)
result = json.loads(eval_response.choices[0].message.content)
if "level" in result:
profile.level = max(1, min(5, int(result["level"])))
if "mastered" in result:
profile.mastered = list(set(profile.mastered + result["mastered"]))
if "struggling" in result:
profile.struggling = list(set(profile.struggling + result["struggling"]))
return profile
Step 5: Wire the session loop
The main loop initializes the profile, seeds the conversation with a greeting, then alternates between student input and tutor output until the student types exit.
def main():
profile = StudentProfile(subject="physics", level=2)
messages = []
print("Tutor: ", end="")
tutoring_turn(messages, profile, "Let's start. What should I know about Newton's first law?")
while True:
try:
user_input = input("\nStudent: ").strip()
except (KeyboardInterrupt, EOFError):
break
if user_input.lower() in {"exit", "quit"}:
break
print("Tutor: ", end="")
tutoring_turn(messages, profile, user_input)
profile = update_profile(profile, messages)
print(
f"[Profile: level={profile.level}, "
f"mastered={profile.mastered}, "
f"struggling={profile.struggling}]"
)
if __name__ == "__main__":
main()
Run it
Save the script as tutor.py, export your key, and run it. Here is a sample session.
$ export OXLO_API_KEY="sk-..."
$ python tutor.py
Tutor: Imagine you are sitting in a car that suddenly stops. Your body lurches forward. Why does that happen?
Student: Because I'm still moving?
Tutor: Exactly. You keep doing what you were doing until something forces you to change. What do we call the tendency of an object to resist changes in motion?
Student: Inertia?
Tutor: Yes. Now, if the same car takes a sharp turn at constant speed, do you feel pushed against the door? And if so, is there a net force on your body at that instant?
[Profile: level=3, mastered=['inertia', "Newton's first law"], struggling=[]]
Notice that the tutor tightened its question after the student demonstrated mastery. Because Oxlo.ai uses flat per-request pricing, we can afford to ship a long adaptive system prompt and a growing message history on every single turn without the cost scaling by token count. For a tutoring product where sessions naturally run long, that pricing model is a structural advantage. You can see the exact plans at https://oxlo.ai/pricing.
Next steps
Swap the in-memory profile for a SQLite table and add a FastAPI layer so students can resume sessions across devices. Or wire in Oxlo.ai's vision models such as Gemma 3 27B or Kimi VL A3B so students can upload photos of handwritten work and receive corrections on the actual page.
Top comments (0)