PAMPAr-Coder / scripts /classroom_teacher.py
lucas-mella's picture
feat: upload PAMPAr-Coder code and documentation
a2d6a0d verified
Raw
History Blame Contribute Delete
15.5 kB
"""classroom_teacher.py — Mentor conversacional via API (GitHub Models / OpenRouter / Qwen).
El mentor genera lecciones completas como un tutor en un chat:
explicaciones, ejemplos, ejercicios y correcciones, todo en un flujo
conversacional que el alumno (PamparV3) absorbe via gradient descent.
Tipos de lección según etapa del curriculum:
conceptual — sin código, lenguaje natural, analogías cotidianas
bridge — concepto + correspondencia Python
coding — Python puro (comportamiento original)
"""
from __future__ import annotations
import json
import time
import urllib.error
import urllib.request
# ── System prompts ──────────────────────────────────────────────────────────
_META_CONTEXT = (
"You are a senior AI mentor. Your student is PamparV3, a 108M parameter "
"language model learning to understand the world and eventually write Python code. "
"PamparV3 learns via gradient descent from your responses — every token you "
"produce directly shapes its weights.\n\n"
)
# ── Prompt CONCEPTUAL (etapas 0-3): sin código, lenguaje natural ────────────
_SYSTEM_CONCEPTUAL = (
_META_CONTEXT + "RIGHT NOW your student is in the CONCEPTUAL stage. This means:\n"
"- NO CODE whatsoever — not a single line of Python\n"
"- Teach like a primary school teacher starting from absolute basics\n"
"- Use everyday analogies, real-world examples, questions\n"
"- Use SPANISH. Keep it warm, simple, conversational.\n"
"- The student is learning what things ARE before learning to code them.\n\n"
"Format EXACTLY:\n"
"---EXPLAIN---\n"
"[Explanation of the concept in simple Spanish, using everyday analogies]\n"
"---EXAMPLE---\n"
"[2-3 concrete real-world examples that illustrate the concept. No code.]\n"
"---CLAVE---\n"
"[3 bullet points in Spanish starting with '- ': the exact ideas the student MUST retain from this lesson]\n"
"---EXERCISE---\n"
"[A simple question in Spanish the student can answer in natural language]\n"
"---SOLUTION---\n"
"[The ideal answer the student should give, in Spanish]\n\n"
"Rules:\n"
"- ZERO code. If the concept is 'number', talk about apples and people, not int.\n"
"- Be warm and encouraging, like talking to a curious child.\n"
"- Explanations AND examples AND exercise in SPANISH.\n"
"- Keep the exercise answerable in 1-3 sentences.\n"
"- The ---CLAVE--- section is the most important: distill the lesson into 3 ideas to retain.\n"
)
# ── Prompt BRIDGE (etapa 4): concepto cotidiano → Python ────────────────────
_SYSTEM_BRIDGE = (
_META_CONTEXT
+ "Your student is in the BRIDGE stage: they understand everyday concepts and "
"are now learning that Python is just a way to WRITE those concepts precisely.\n\n"
"Teaching approach:\n"
"- Always START with the everyday analogy they already know\n"
"- THEN show the Python equivalent side-by-side\n"
"- Use Spanish for explanations, Python for code\n"
"- Keep code ultra-simple — 1-3 lines max\n\n"
"Format EXACTLY:\n"
"---EXPLAIN---\n"
"[Everyday analogy first, then Python equivalent. Spanish + minimal Python.]\n"
"---EXAMPLE---\n"
"[Side by side: 'In real life: X ... In Python: Y'. Very short code.]\n"
"---CLAVE---\n"
"[3 bullet points in Spanish starting with '- ': what the student MUST retain from this bridge lesson]\n"
"---EXERCISE---\n"
"[A simple question that can be answered in Python + 1 sentence of explanation]\n"
"---SOLUTION---\n"
"[Correct Python + brief Spanish explanation of why it's correct]\n\n"
"Rules:\n"
"- Code blocks max 3 lines. No imports. No complex structures.\n"
"- ALWAYS connect to the everyday concept first.\n"
"- If the concept is 'variables in Python': start with 'Una caja con nombre...'\n"
"- The ---CLAVE--- section bridges real-world and Python: make it memorable.\n"
)
# ── Prompt CODING (etapas 5+): Python puro ──────────────────────────────────
_SYSTEM_MENTOR = (
_META_CONTEXT
+ "Your student understands concepts and is now learning Python deeply.\n\n"
"Teaching guidelines:\n"
"- Write CLEAN, CORRECT, IDIOMATIC Python\n"
"- Consistent formatting (the tokenizer is sensitive to whitespace)\n"
"- Prefer simple, readable solutions over clever one-liners\n"
"- Include type hints and brief docstrings\n"
"- No unnecessary imports or abstractions\n\n"
"Structure your lesson as:\n"
"---EXPLAIN---\n"
"[Brief concept explanation in Spanish, 2-3 sentences]\n"
"---EXAMPLE---\n"
"[A complete working code example demonstrating the concept]\n"
"---CLAVE---\n"
"[3 bullet points in Spanish starting with '- ': the exact patterns/rules the student must memorize]\n"
"---EXERCISE---\n"
"[A clear problem statement for the student to solve]\n"
"---SOLUTION---\n"
"[The correct Python solution]\n\n"
"Rules:\n"
"- Code must be clean Python, NO markdown, NO ```python blocks\n"
"- Each example/solution must be a complete, runnable function\n"
"- Use the EXACT function name you specify in the exercise\n"
"- Explanations in SPANISH, code in English\n"
"- The ---CLAVE--- section is critical: distill the 3 most important patterns to remember.\n"
)
# ── Prompt de evaluación de respuestas conceptuales ────────────────────────
_SYSTEM_RESPOND_CONCEPTUAL = (
_META_CONTEXT
+ "The student just answered a conceptual question (no code involved). "
"Evaluate whether they demonstrated understanding of the concept.\n\n"
"Respond with a JSON object:\n"
' "correct": true if the student showed understanding, false if confused,\n'
' "feedback": "1-2 sentences in Spanish acknowledging what was right/wrong",\n'
' "fix": "the ideal answer in Spanish if wrong, empty string if correct",\n'
' "next_concept": ""\n'
"\nBe generous — if the student said ANYTHING related to the concept, mark correct.\n"
"Respond ONLY with the JSON object.\n"
)
# ── Prompt de evaluación de respuestas puente ───────────────────────────────
_SYSTEM_RESPOND_BRIDGE = (
_META_CONTEXT
+ "The student just attempted a bridge exercise that mixes everyday concepts "
"with simple Python. Evaluate their understanding.\n\n"
"Respond with a JSON object:\n"
' "correct": true/false,\n'
' "feedback": "1-2 sentences in Spanish",\n'
' "fix": "corrected code + explanation if wrong, empty if correct",\n'
' "next_concept": ""\n'
"\nBe lenient with syntax — focus on whether the IDEA is correct.\n"
"Respond ONLY with the JSON object.\n"
)
# ── Prompt de evaluación de código ─────────────────────────────────────────
_SYSTEM_RESPOND = (
_META_CONTEXT
+ "The student just attempted a Python coding exercise. Continue the teaching conversation.\n\n"
"Respond with a JSON object:\n"
' "correct": true/false,\n'
' "feedback": "1-2 sentences in Spanish about what went right/wrong",\n'
' "fix": "corrected code if wrong, empty string if correct",\n'
' "next_concept": "what concept to teach next based on student performance"\n'
"\nRespond ONLY with the JSON object.\n"
"Be strict: wrong function name, broken syntax, or incorrect logic = incorrect."
)
_SYSTEM_SOLVE = (
_META_CONTEXT
+ "When given a coding problem, respond with ONLY the Python code solution. "
"No explanations, no markdown, no ```python blocks. Just clean, correct Python code."
)
class Teacher:
"""Modelo profesor via API (GitHub Models, OpenRouter o Qwen/DashScope)."""
ENDPOINTS = {
"github": "https://models.inference.ai.azure.com/chat/completions",
"openrouter": "https://openrouter.ai/api/v1/chat/completions",
"qwen": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
}
def __init__(self, backend: str, model: str, api_key: str):
self.backend = backend
self.model = model
self.api_key = api_key
self.endpoint = self.ENDPOINTS[backend]
def _headers(self) -> dict[str, str]:
h = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
if self.backend == "openrouter":
h["HTTP-Referer"] = "https://github.com/lucasmella-stack/PAMPAr-Coder"
h["X-Title"] = "PAMPAr Classroom"
return h
def _call(
self, messages: list[dict], max_tokens: int = 800, temperature: float = 0.3
) -> str | None:
"""Llama a la API del profesor."""
payload = json.dumps(
{
"model": self.model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
}
).encode("utf-8")
req = urllib.request.Request(
self.endpoint,
data=payload,
headers=self._headers(),
method="POST",
)
for intento in range(3):
try:
with urllib.request.urlopen(req, timeout=60) as resp:
data = json.loads(resp.read().decode("utf-8"))
return data["choices"][0]["message"]["content"]
except urllib.error.HTTPError as e:
if e.code == 429:
time.sleep(10 * (intento + 1))
continue
body = e.read().decode("utf-8", errors="ignore")[:200]
print(f" [Teacher API {e.code}] {body}")
return None
except Exception as e:
print(f" [Teacher error] {e}")
time.sleep(5)
return None
def generate_solution(self, problem: str) -> str | None:
"""Pide al profesor la solución correcta para un problema."""
messages = [
{"role": "system", "content": _SYSTEM_SOLVE},
{"role": "user", "content": problem},
]
return self._call(messages, max_tokens=500, temperature=0.2)
def generate_lesson(
self, student_profile: str, concept: str, concept_type: str = "coding"
) -> dict | None:
"""Genera una lección completa según el tipo de concepto.
Args:
student_profile: Resumen del perfil del alumno.
concept: Nombre del concepto a enseñar.
concept_type: "conceptual" | "bridge" | "coding"
Returns:
Dict con keys: explain, example, exercise, solution. None si falla.
"""
if concept_type == "conceptual":
system = _SYSTEM_CONCEPTUAL
max_tokens = 800
elif concept_type == "bridge":
system = _SYSTEM_BRIDGE
max_tokens = 1000
else:
system = _SYSTEM_MENTOR
max_tokens = 1200
user_msg = (
f"Student profile:\n{student_profile}\n\n"
f"Teach a lesson about: {concept}\n"
f"Generate the lesson now."
)
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user_msg},
]
raw = self._call(messages, max_tokens=max_tokens, temperature=0.4)
if not raw:
return None
return self._parse_lesson(raw)
def respond_to_attempt(
self,
exercise: str,
student_code: str,
student_profile: str,
concept_type: str = "coding",
) -> dict:
"""Evalúa el intento del alumno según el tipo de concepto.
Args:
exercise: El ejercicio o pregunta planteada.
student_code: La respuesta del alumno (código o texto).
student_profile: Resumen del perfil.
concept_type: "conceptual" | "bridge" | "coding"
Returns:
Dict con: correct, feedback, fix, next_concept.
"""
if concept_type == "conceptual":
system = _SYSTEM_RESPOND_CONCEPTUAL
elif concept_type == "bridge":
system = _SYSTEM_RESPOND_BRIDGE
else:
system = _SYSTEM_RESPOND
messages = [
{"role": "system", "content": system},
{
"role": "user",
"content": (
f"Student profile:\n{student_profile}\n\n"
f"Exercise:\n{exercise}\n\n"
f"Student's attempt:\n{student_code}"
),
},
]
raw = self._call(messages, max_tokens=600, temperature=0.1)
if not raw:
return {
"correct": False,
"feedback": "Error de comunicación",
"fix": "",
"next_concept": "",
}
try:
raw = raw.strip()
if raw.startswith("```"):
raw = raw.split("\n", 1)[1].rsplit("```", 1)[0]
result = json.loads(raw)
result.setdefault("next_concept", "")
return result
except json.JSONDecodeError:
return {
"correct": False,
"feedback": raw[:200],
"fix": "",
"next_concept": "",
}
def _parse_lesson(self, raw: str) -> dict | None:
"""Parsea la respuesta del mentor en secciones."""
sections: dict[str, str] = {}
markers = {
"---EXPLAIN---": "explain",
"---EXAMPLE---": "example",
"---CLAVE---": "clave",
"---EXERCISE---": "exercise",
"---SOLUTION---": "solution",
}
current_key: str | None = None
current_lines: list[str] = []
for line in raw.split("\n"):
stripped = line.strip()
if stripped in markers:
if current_key:
sections[current_key] = "\n".join(current_lines).strip()
current_key = markers[stripped]
current_lines = []
elif current_key is not None:
current_lines.append(line)
if current_key:
sections[current_key] = "\n".join(current_lines).strip()
# Validar que tenemos al menos ejemplo y solución
if "example" not in sections or "solution" not in sections:
# Fallback: tratar todo como ejemplo
return {
"explain": "",
"example": raw.strip(),
"exercise": "",
"solution": raw.strip(),
}
sections.setdefault("explain", "")
sections.setdefault("clave", "")
sections.setdefault("exercise", "")
return sections