We are going to build an adaptive Socratic tutor that helps middle school students reason through biology concepts instead of memorizing answers. The agent maintains conversation memory, evaluates whether the student actually understands the material, and adjusts its difficulty automatically. If you are building homework helpers or classroom chatbots, this pattern drops directly into your EdTech stack.
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: Configure the Oxlo.ai client
I start by instantiating the OpenAI-compatible client pointing at Oxlo.ai. Because Oxlo.ai has no cold starts on popular models, the first request after idle time responds immediately, which matters for classroom settings where students do not wait.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
# Quick connectivity check
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "user", "content": "Say hello"}
],
)
print(response.choices[0].message.content)
Step 2: Write the Socratic system prompt
The system prompt is the only curriculum the model receives. It enforces Socratic questioning, sets a word limit to keep student attention, and instructs the model to track whether the student is struggling, progressing, or has mastered the topic.
SYSTEM_PROMPT = """
You are a middle school biology tutor. Follow these rules exactly:
1. Never give the final answer directly. Guide the student with questions.
2. Keep every response under 120 words.
3. Track the student's understanding internally as struggling, progressing, or mastered.
4. If struggling, use simpler vocabulary and break concepts into smaller parts.
5. If progressing, ask one targeted follow-up question.
6. If mastered, confirm briefly and ask if they want to move to the next topic.
"""
Step 3: Build the interactive loop with memory
We need a loop that sends the student message, appends the assistant reply to shared history, and prints it. I keep the last ten exchanges so the model remembers earlier hints without drowning in context. On Oxlo.ai, request-based pricing means this history does not increase the cost per turn, unlike token-based providers where long sessions get expensive quickly. Details are at https://oxlo.ai/pricing.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
SYSTEM_PROMPT = """
You are a middle school biology tutor. Follow these rules exactly:
1. Never give the final answer directly. Guide the student with questions.
2. Keep every response under 120 words.
3. Track the student's understanding internally as struggling, progressing, or mastered.
4. If struggling, use simpler vocabulary and break concepts into smaller parts.
5. If progressing, ask one targeted follow-up question.
6. If mastered, confirm briefly and ask if they want to move to the next topic.
"""
history = []
def tutor_turn(student_input):
history.append({"role": "user", "content": student_input})
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "system", "content": SYSTEM_PROMPT}] + history,
)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply})
# Prevent unbounded context growth
if len(history) > 20:
history.pop(0)
history.pop(0)
return reply
if __name__ == "__main__":
print("Tutor: Let's discuss photosynthesis. What do you think plants eat?")
while True:
user_msg = input("Student: ")
if user_msg.lower() in ["exit", "quit"]:
break
print("Tutor:", tutor_turn(user_msg))
Step 4: Add mastery checks and difficulty adaptation
After each exchange, I run a short classification call against the conversation history to decide if the student is struggling, progressing, or has mastered the concept. If they mastered it, we offer the next topic. If they are struggling, we inject a level note into the system context. Because this is a separate API call, a flat per-request price keeps the cost predictable even when we double the calls per session.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
SYSTEM_PROMPT = """
You are a middle school biology tutor. Follow these rules exactly:
1. Never give the final answer directly. Guide the student with questions.
2. Keep every response under 120 words.
3. Track the student's understanding internally as struggling, progressing, or mastered.
4. If struggling, use simpler vocabulary and break concepts into smaller parts.
5. If progressing, ask one targeted follow-up question.
6. If mastered, confirm briefly and ask if they want to move to the next topic.
"""
def get_level(history):
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=history + [
{"role": "user", "content": "Classify the student's understanding as exactly one word: struggling, progressing, or mastered. No explanation."}
],
)
text = response.choices[0].message.content.strip().lower()
if "mastered" in text:
return "mastered"
if "struggling" in text:
return "struggling"
return "progressing"
def tutor_turn(history, student_input, level):
level_note = f"The student is currently classified as: {level}. Adjust your guidance accordingly."
messages = [
{"role": "system", "content": SYSTEM_PROMPT + "\n" + level_note},
] + history + [
{"role": "user", "content": student_input},
]
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=messages,
)
return response.choices[0].message.content
if __name__ == "__main__":
history = []
level = "progressing"
topic = "photosynthesis"
print(f"Tutor: Let's talk about {topic}. What do you already know about how plants make their food?")
while True:
user_msg = input("Student: ")
if user_msg.lower() in ["exit", "quit"]:
break
reply = tutor_turn(history, user_msg, level)
print(f"Tutor: {reply}")
history.append({"role": "user", "content": user_msg})
history.append({"role": "assistant", "content": reply})
if len(history) > 20:
history.pop(0)
history.pop(0)
level = get_level(history)
if level == "mastered":
print("Tutor: You seem to have mastered this. Want to move to cellular respiration? (yes/exit)")
nxt = input("Student: ")
if nxt.lower() == "yes":
topic = "cellular respiration"
history = []
level = "progressing"
print(f"Tutor: Great. Let's start {topic}. What happens to your breathing when you run?")
else:
break
Run it
Save the final script as tutor.py, export your key, and run it. Here is a sample session:
$ export OXLO_API_KEY="sk-oxlo.ai-..."
$ python tutor.py
Tutor: Let's talk about photosynthesis. What do you already know about how plants make their food?
Student: They eat dirt?
Tutor: That's a common guess. Let's think about what actually enters a plant's leaves from the air. Do you know what gas humans breathe out that plants might use?
Student: Carbon dioxide?
Tutor: Exactly. Now, what energy source helps the plant turn that gas into food?
Student: The sun?
Tutor: Right. So if a plant has sunlight, carbon dioxide, and water, what do you think it produces?
Student: Oxygen and sugar?
Tutor: You seem to have mastered this. Want to move to cellular respiration? (yes/exit)
Student: yes
Tutor: Great. Let's start cellular respiration. What happens to your breathing when you run?
Wrap-up and next steps
The agent now handles multi-turn reasoning, memory, and adaptive pacing. A concrete next step is to wrap this loop in a FastAPI endpoint and store sessions in Redis so a classroom of students can use it simultaneously. You could also swap the model ID to kimi-k2.6 or deepseek-v3.2 on Oxlo.ai for heavier reasoning workloads, or add vision support with gemma-3-27b so students can upload diagrams of the Calvin cycle and ask questions about them.
Top comments (0)