We are going to build a terminal-based adaptive tutor that diagnoses a student's understanding of any topic and then teaches through targeted Socratic questioning. I shipped this during a weekend ed-tech hackathon to keep tutoring sessions cheap enough to run for thousands of daily active users. Because Oxlo.ai charges a flat rate per request instead of per token (see pricing), long multi-turn conversations with detailed explanations do not explode in cost.
What you'll need
- Python 3.10 or newer
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK:
pip install openai
Step 1: Scaffold the student session
We need a small class to track the conversation history and a live profile of the student's strengths and misconceptions. This state stays in memory for now.
from openai import OpenAI
import json
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
class TutorSession:
def __init__(self, topic: str):
self.topic = topic
self.messages = []
self.profile = {
"topic": topic,
"level": "unknown",
"strengths": [],
"misconceptions": [],
"questions_asked": 0
}
def update_profile(self, analysis: dict):
if "level" in analysis:
self.profile["level"] = analysis["level"]
self.profile["strengths"].extend(analysis.get("strengths", []))
self.profile["misconceptions"].extend(analysis.get("misconceptions", []))
self.profile["questions_asked"] += 1
Step 2: Define the tutor system prompt
The system prompt constrains the model to act as a diagnostic tutor. It must return structured JSON for the profile updates and plain text for the student.
SYSTEM_PROMPT = """You are an adaptive tutor. Your goal is to teach the student using Socratic questioning.
Rules:
1. First, ask one diagnostic question to gauge prior knowledge.
2. After each student answer, output a JSON block inside triple backticks containing:
- "level": one of "beginner", "intermediate", "advanced"
- "strengths": list of strings
- "misconceptions": list of strings
3. Then, in the same response, write a brief follow-up question or explanation for the student. Do not give the full answer immediately.
4. Adapt difficulty based on the student's level.
5. Keep responses under 120 words.
Topic: {topic}
Current student profile: {profile}
"""
Step 3: Build the diagnostic turn
We initialize the session with the student's chosen topic and send the first request to get a diagnostic question.
def start_session(session: TutorSession) -> str:
prompt = SYSTEM_PROMPT.format(
topic=session.topic,
profile=json.dumps(session.profile, indent=2)
)
session.messages = [
{"role": "system", "content": prompt},
{"role": "user", "content": "Start the lesson."}
]
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=session.messages,
)
reply = response.choices[0].message.content
session.messages.append({"role": "assistant", "content": reply})
return reply
Step 4: Parse feedback and update the profile
Each assistant reply contains a JSON block for internal state and plain text for the student. We parse the JSON, update the profile, and refresh the system prompt so the model sees the latest summary on the next turn.
import re
def parse_reply(raw: str) -> tuple[dict, str]:
json_match = re.search(r"
```json\s*(.*?)\s*```
", raw, re.DOTALL)
if json_match:
json_text = json_match.group(1)
student_text = re.sub(r"
```json\s*.*?\s*```
", "", raw, flags=re.DOTALL).strip()
return json.loads(json_text), student_text
return {}, raw.strip()
def refresh_system_prompt(session: TutorSession):
new_prompt = SYSTEM_PROMPT.format(
topic=session.topic,
profile=json.dumps(session.profile, indent=2)
)
session.messages[0] = {"role": "system", "content": new_prompt}
def handle_student_answer(session: TutorSession, answer: str) -> str:
session.messages.append({"role": "user", "content": answer})
refresh_system_prompt(session)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=session.messages,
)
raw = response.choices[0].message.content
session.messages.append({"role": "assistant", "content": raw})
analysis, student_text = parse_reply(raw)
session.update_profile(analysis)
return student_text
Step 5: Add the CLI loop
We wire everything into a simple REPL that runs until the student types "exit". The profile is printed at the end so you can verify the adaptation.
def run_tutor(topic: str):
session = TutorSession(topic)
print(f"Starting adaptive tutor for: {topic}")
print("-" * 40)
first_reply = start_session(session)
_, student_text = parse_reply(first_reply)
print(f"Tutor: {student_text}")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("exit", "quit"):
break
reply = handle_student_answer(session, user_input)
print(f"Tutor: {reply}")
print("-" * 40)
print("Final profile:")
print(json.dumps(session.profile, indent=2))
if __name__ == "__main__":
run_tutor("photosynthesis")
Run it
Save the script as tutor.py, replace YOUR_OXLO_API_KEY, and run:
python tutor.py
Example session output:
Starting adaptive tutor for: photosynthesis
----------------------------------------
Tutor: Before we dive in, can you tell me what you think plants need to make their own food, and where that process happens in the plant?
You: They need sunlight and water, and it happens in the leaves.
Tutor: Good, you identified two key inputs. Do you know what gas plants take in from the air through tiny openings in those leaves?
You: Oxygen?
Tutor: Not quite. Plants do use oxygen for respiration, but they take in a different gas for photosynthesis. Can you recall which one?
You: Carbon dioxide?
Tutor: Exactly. Now, combining sunlight, water, and carbon dioxide, plants produce glucose and another byproduct. What gets released back into the air?
You: Oxygen?
Tutor: Correct. You have moved from a beginner to an intermediate understanding. One more question: why is chlorophyll green?
You: exit
----------------------------------------
Final profile:
{
"topic": "photosynthesis",
"level": "intermediate",
"strengths": [
"knows inputs: sunlight and water",
"knows location: leaves",
"recalled carbon dioxide",
"identified oxygen byproduct"
],
"misconceptions": [
"initially confused photosynthesis gas with oxygen"
],
"questions_asked": 4
}
Wrap-up and next steps
This agent works because the flat per-request pricing on Oxlo.ai makes it practical to run long, branching tutoring sessions without counting tokens. If you want to take this further, persist the student profile to SQLite between sessions so the tutor remembers a learner across days. You could also swap in qwen-3-32b for stronger multilingual support if your students ask questions in other languages.
Top comments (0)