| """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
|
|
|
|
|
|
|
| _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"
|
| )
|
|
|
|
|
| _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"
|
| )
|
|
|
|
|
| _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"
|
| )
|
|
|
|
|
| _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"
|
| )
|
|
|
|
|
| _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"
|
| )
|
|
|
|
|
| _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"
|
| )
|
|
|
|
|
| _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()
|
|
|
|
|
| if "example" not in sections or "solution" not in sections:
|
|
|
| return {
|
| "explain": "",
|
| "example": raw.strip(),
|
| "exercise": "",
|
| "solution": raw.strip(),
|
| }
|
|
|
| sections.setdefault("explain", "")
|
| sections.setdefault("clave", "")
|
| sections.setdefault("exercise", "")
|
| return sections
|
|
|