PAMPAr-Coder / scripts /classroom_curriculum.py
lucas-mella's picture
feat: upload PAMPAr-Coder code and documentation
a2d6a0d verified
Raw
History Blame Contribute Delete
28.1 kB
"""classroom_curriculum.py β€” ConfiguraciΓ³n, Γ‘rbol de conceptos y perfil del alumno."""
from __future__ import annotations
import random
from collections import defaultdict
from dataclasses import dataclass, field
@dataclass
class ClassroomConfig:
"""ConfiguraciΓ³n del aula."""
# Modelo
checkpoint_in: str = "checkpoints/v3_ghidra_v9.pt"
checkpoint_out: str = "checkpoints/v3_classroom.pt"
device: str = "auto"
# Teacher
teacher_backend: str = "github" # "github" | "openrouter"
teacher_model: str = "openai/gpt-4o-mini"
api_key: str = ""
# Entrenamiento bio-inspirado
lr_base: float = 5e-6 # LR base (conservador)
lr_llaves_mult: float = 0.01 # LLAVES/TΓ‘lamo: 1% del LR base
lr_attn_mult: float = 0.1 # AtenciΓ³n: 10% del LR base
lr_embed_mult: float = 0.1 # Embeddings: 10% del LR base
lr_ffn_mult: float = 1.0 # FFN/StreamFFN: 100% del LR base
# EWC
ewc_lambda: float = 500.0 # Fuerza de la penalizaciΓ³n EWC
ewc_samples: int = 200 # Muestras para calcular Fisher
# Replay buffer
replay_size: int = 100 # TamaΓ±o del buffer
replay_ratio: float = 0.5 # 50% replay, 50% nuevo
# Curriculum
start_level: int = 1 # Nivel inicial (1-5)
advance_threshold: float = 0.7 # 70% correcto para avanzar
window_size: int = 10 # Ventana para calcular accuracy
# SesiΓ³n
max_lessons: int = 200 # MΓ‘ximo de lecciones por sesiΓ³n
guardar_cada: int = 20 # Guardar checkpoint cada N lecciones
seq_len: int = 256 # Longitud mΓ‘x de secuencia para training
# Bio-inspired mechanisms
bio_enabled: bool = True # Activar mecanismos bio-inspirados
sleep_every: int = 15 # ConsolidaciΓ³n de sueΓ±o cada N lecciones
prune_every: int = 30 # Poda sinΓ‘ptica cada N lecciones
# Server
port: int = 8888
# Recording
record: bool = True # Grabar sesiΓ³n como video reproducible
CURRICULUM: dict[int, dict] = {
1: {
"nombre": "Fundamentos",
"desc": "Variables, funciones simples, operaciones bΓ‘sicas",
"ejercicios": [
"Write a Python function `suma(a, b)` that returns the sum of two numbers.",
"Write a Python function `es_par(n)` that returns True if n is even, False otherwise.",
"Write a Python function `longitud(texto)` that returns the length of a string without using len().",
"Write a Python function `invertir(texto)` that returns the reversed string.",
"Write a Python function `contar_vocales(texto)` that counts vowels (a,e,i,o,u) case-insensitive.",
"Write a Python function `suma_digitos(n)` that returns the sum of all digits of a non-negative integer.",
"Write a Python function `es_palindromo(s)` that returns True if the string is a palindrome.",
"Write a Python function `maximo(a, b)` that returns the larger of two numbers without using max().",
"Write a Python function `absoluto(n)` that returns the absolute value without using abs().",
"Write a Python function `celsius_a_fahrenheit(c)` that converts Celsius to Fahrenheit.",
"Write a Python function `factorial(n)` that returns n! using a loop.",
"Write a Python function `potencia(base, exp)` that returns base**exp using a loop.",
"Write a Python function `duplicar_lista(lst)` that returns a new list with each element doubled.",
"Write a Python function `minimo_lista(lst)` that returns the smallest element without using min().",
"Write a Python function `contar_mayusculas(texto)` that counts uppercase letters in a string.",
],
},
2: {
"nombre": "Estructuras de control",
"desc": "Loops, condicionales, listas, diccionarios",
"ejercicios": [
"Write a Python function `fizzbuzz(n)` that returns 'FizzBuzz' if n divisible by 3 and 5, 'Fizz' if by 3, 'Buzz' if by 5, else str(n).",
"Write a Python function `fibonacci(n)` that returns the n-th Fibonacci number (0-indexed).",
"Write a Python function `frecuencia(lista)` that returns a dict mapping each element to its count.",
"Write a Python function `aplanar(lista)` that flattens a list of lists by one level.",
"Write a Python function `cuadrados_pares(n)` that returns squares of all even numbers from 2 to n.",
"Write a Python function `invertir_dict(d)` that returns a new dict with keys and values swapped.",
"Write a Python function `busqueda_lineal(lista, objetivo)` that returns the index or -1 if not found.",
"Write a Python function `eliminar_duplicados(lista)` that returns a list without duplicates, preserving order.",
"Write a Python function `es_primo(n)` that returns True if n is prime.",
"Write a Python function `ordenar_burbuja(lista)` that sorts a list using bubble sort.",
"Write a Python function `interseccion(a, b)` that returns elements common to both lists.",
"Write a Python function `rotar_lista(lst, k)` that rotates list left by k positions.",
],
},
3: {
"nombre": "Funciones avanzadas",
"desc": "RecursiΓ³n, generadores, comprensiones complejas",
"ejercicios": [
"Write a Python function `merge_sort(lista)` that returns a new sorted list using merge sort.",
"Write a Python function `busqueda_binaria(lista, objetivo)` that returns the index or -1.",
"Write a Python function `primos_hasta(n)` that yields all primes up to n using a generator.",
"Write a Python function `memoize(fn)` that returns a cached version of fn.",
"Write a Python function `aplanar_profundo(lst)` that recursively flattens nested lists.",
"Write a Python function `permutaciones(lst)` that returns all permutations of a list.",
"Write a Python function `cifrado_cesar(texto, k)` that shifts each letter by k positions.",
"Write a Python function `potencia_recursiva(base, exp)` that calculates power recursively.",
"Write a Python function `torre_hanoi(n, origen, destino, auxiliar)` that prints the moves.",
"Write a Python function `zip_manual(a, b)` that zips two lists without using zip().",
],
},
4: {
"nombre": "Clases y OOP",
"desc": "Clases, herencia, mΓ©todos especiales",
"ejercicios": [
"Write a Python class `Stack` with methods `push(item)`, `pop()`, `is_empty()`, `peek()`.",
"Write a Python class `Punto` with x, y attributes and a method `distancia(otro)` for Euclidean distance.",
"Write a Python class `Cola` implementing a FIFO queue with `enqueue(item)` and `dequeue()`.",
"Write a Python class `Fraccion` with add, sub, mul, and __str__ using GCD simplification.",
"Write a Python class `Contador` that counts how many times it has been called (using __call__).",
"Write a Python class `Vector` with __add__, __sub__, __mul__ (scalar) and __repr__.",
"Write a Python class `ListaEnlazada` with `agregar(valor)`, `buscar(valor)`, `__len__`.",
"Write a Python class `Matriz` with __add__ and __mul__ for 2D matrix operations.",
],
},
5: {
"nombre": "Patrones avanzados",
"desc": "Decoradores, context managers, algoritmos complejos",
"ejercicios": [
"Write a Python decorator `cronometrar` that prints how long a function takes to execute.",
"Write a Python context manager class `TempFile` that creates a temp file and deletes it on exit.",
"Write a Python function `lru_cache(maxsize)` decorator that caches the last maxsize unique calls.",
"Write a Python function `dijkstra(grafo, inicio)` that returns shortest distances from inicio.",
"Write a Python async function `fetch_all(urls)` that fetches URLs concurrently with asyncio.",
"Write a Python function `quick_sort(lista)` implementing quicksort with median-of-three pivot.",
],
},
}
# =============================================================================
# Árbol de conceptos β€” el mentor elige quΓ© enseΓ±ar basΓ‘ndose en esto
# =============================================================================
# Tipos de concepto:
# "conceptual" β€” sin cΓ³digo, lenguaje natural (etapas 0-3)
# "bridge" β€” mezcla: concepto cotidiano + correspondencia Python (etapa 4)
# "coding" β€” Python puro (etapas 5+, curriculum original)
#
# Orden de prerequisitos: cada concepto requiere dominar los anteriores en su grupo
CONCEPT_TREE: list[dict] = [
# =========================================================================
# ETAPA 0 β€” PRESENCIA Y COMUNICACIΓ“N (sin cΓ³digo)
# El alumno aprende que existe, que puede responder, que hay un interlocutor.
# =========================================================================
{
"id": "greeting",
"name": "Saludos y presentaciΓ³n",
"type": "conceptual",
"stage": 0,
"desc": "Intercambio de saludos. El alumno aprende a responder y articular quiΓ©n es.",
"prereqs": [],
},
# =========================================================================
# ETAPA 1 β€” CONCEPTOS PRIMITIVOS (sin cΓ³digo)
# Lo mismo que aprender letras y nΓΊmeros antes de aprender a leer.
# =========================================================================
{
"id": "concept_number",
"name": "ΒΏQuΓ© es un nΓΊmero?",
"type": "conceptual",
"stage": 1,
"desc": "Un nΓΊmero representa una cantidad real: 1 manzana, 3 personas, 0 lluvia.",
"prereqs": ["greeting"],
},
{
"id": "concept_sequence",
"name": "Secuencias y orden",
"type": "conceptual",
"stage": 1,
"desc": "Una secuencia es algo que viene en orden. Contar: 1, 2, 3... Los dΓ­as de la semana.",
"prereqs": ["concept_number"],
},
{
"id": "concept_word",
"name": "Palabras y significado",
"type": "conceptual",
"stage": 1,
"desc": "Una palabra es un sΓ­mbolo con significado: 'casa', 'rojo', 'correr'.",
"prereqs": ["greeting"],
},
{
"id": "concept_true_false",
"name": "Verdadero y Falso",
"type": "conceptual",
"stage": 1,
"desc": "Solo hay dos opciones: algo es verdad o mentira. El cielo es azul: verdad. Las vacas vuelan: mentira.",
"prereqs": ["greeting"],
},
# =========================================================================
# ETAPA 2 β€” LΓ“GICA COTIDIANA (sin cΓ³digo)
# Como aprender a combinar letras en sΓ­labas.
# =========================================================================
{
"id": "concept_compare",
"name": "Comparar cosas",
"type": "conceptual",
"stage": 2,
"desc": "Mayor, menor, igual. 5 es mayor que 3. Una jirafa es mΓ‘s alta que un gato.",
"prereqs": ["concept_number", "concept_word"],
},
{
"id": "concept_if_then",
"name": "Si... entonces...",
"type": "conceptual",
"stage": 2,
"desc": "Una regla: SI llueve, ENTONCES llevΓ‘s paraguas. SI tienes hambre, ENTONCES comΓ©s.",
"prereqs": ["concept_true_false"],
},
{
"id": "concept_repeat",
"name": "Repetir acciones",
"type": "conceptual",
"stage": 2,
"desc": "A veces repetimos lo mismo varias veces hasta que algo cambia. Contar, respirar, caminar.",
"prereqs": ["concept_sequence"],
},
# =========================================================================
# ETAPA 3 β€” PROCEDIMIENTOS Y ABSTRACCIΓ“N (sin cΓ³digo)
# Como combinar sΓ­labas en palabras y palabras en oraciones.
# =========================================================================
{
"id": "concept_steps",
"name": "Recetas y procedimientos",
"type": "conceptual",
"stage": 3,
"desc": "Una receta es una lista de pasos ordenados que llevan a un resultado. Primero X, luego Y.",
"prereqs": ["concept_sequence", "concept_if_then"],
},
{
"id": "concept_variable_box",
"name": "Cajas con nombre (variables)",
"type": "conceptual",
"stage": 3,
"desc": "Una caja con etiqueta que guarda algo. La caja 'nombre' guarda 'Ana'. La caja 'edad' guarda 25.",
"prereqs": ["concept_number", "concept_word"],
},
{
"id": "concept_function_machine",
"name": "MΓ‘quinas que procesan (funciones)",
"type": "conceptual",
"stage": 3,
"desc": "Una mΓ‘quina recibe algo, lo transforma, y devuelve algo. Una licuadora: recibe fruta, devuelve jugo.",
"prereqs": ["concept_steps"],
},
# =========================================================================
# ETAPA 4 β€” PUENTE: CONCEPTOS β†’ CΓ“DIGO (mezcla de lenguaje y Python)
# Como pasar de leer oraciones simples a leer textos mΓ‘s formales.
# =========================================================================
{
"id": "code_intro",
"name": "Python: instrucciones para la computadora",
"type": "bridge",
"stage": 4,
"desc": "Python es cΓ³mo le escribimos instrucciones a la computadora, igual que una receta.",
"prereqs": ["concept_function_machine", "concept_variable_box"],
},
{
"id": "code_values",
"name": "NΓΊmeros y texto en Python",
"type": "bridge",
"stage": 4,
"desc": "Los nΓΊmeros son iguales: 5, 3.14. El texto va entre comillas: 'hola'.",
"prereqs": ["code_intro"],
},
{
"id": "code_true_false",
"name": "True y False en Python",
"type": "bridge",
"stage": 4,
"desc": "Verdadero se escribe True. Falso se escribe False. Es lo mismo que sΓ­/no.",
"prereqs": ["code_intro", "concept_true_false"],
},
{
"id": "code_variables",
"name": "Variables en Python",
"type": "bridge",
"stage": 4,
"desc": "edad = 25 β†’ la caja 'edad' ahora guarda 25. nombre = 'Ana'.",
"prereqs": ["concept_variable_box", "code_values"],
},
{
"id": "code_if",
"name": "if/else en Python",
"type": "bridge",
"stage": 4,
"desc": "El 'si... entonces...' cotidiano se escribe: if condicion: ... else: ...",
"prereqs": ["concept_if_then", "code_true_false"],
},
# =========================================================================
# ETAPA 5+ β€” CΓ“DIGO PYTHON (curriculum original, tipo "coding")
# =========================================================================
# Nivel 1 β€” Fundamentos
{
"id": "arithmetic",
"name": "Arithmetic operations",
"type": "coding",
"stage": 5,
"desc": "suma, resta, multiplicaciΓ³n, divisiΓ³n, mΓ³dulo, potencia",
"prereqs": ["code_variables"],
},
{
"id": "variables_types",
"name": "Variables and types",
"type": "coding",
"stage": 5,
"desc": "int, float, str, bool, type conversion, f-strings",
"prereqs": ["arithmetic"],
},
{
"id": "conditionals",
"name": "Conditionals",
"type": "coding",
"stage": 5,
"desc": "if/elif/else, comparadores, operadores lΓ³gicos (and, or, not)",
"prereqs": ["variables_types"],
},
{
"id": "strings",
"name": "String operations",
"type": "coding",
"stage": 5,
"desc": "slicing, split, join, replace, find, lower/upper, f-strings",
"prereqs": ["variables_types"],
},
{
"id": "functions_basic",
"name": "Basic functions",
"type": "coding",
"stage": 5,
"desc": "def, parΓ‘metros, return, valores por defecto, docstrings",
"prereqs": ["variables_types"],
},
# Nivel 6 β€” Control de flujo
{
"id": "loops_for",
"name": "For loops",
"type": "coding",
"stage": 6,
"desc": "for, range, enumerate, iteraciΓ³n sobre secuencias",
"prereqs": ["functions_basic", "conditionals"],
},
{
"id": "loops_while",
"name": "While loops",
"type": "coding",
"stage": 6,
"desc": "while, break, continue, centinela, acumulador",
"prereqs": ["loops_for"],
},
{
"id": "lists",
"name": "Lists",
"type": "coding",
"stage": 6,
"desc": "crear, indexar, append, extend, slicing, list comprehensions",
"prereqs": ["loops_for"],
},
{
"id": "tuples_sets",
"name": "Tuples and sets",
"type": "coding",
"stage": 6,
"desc": "tuplas inmutables, sets, operaciones de conjuntos",
"prereqs": ["lists"],
},
{
"id": "dicts",
"name": "Dictionaries",
"type": "coding",
"stage": 6,
"desc": "crear, acceder, items, keys, values, dict comprehensions",
"prereqs": ["lists"],
},
# Nivel 7 β€” Funciones avanzadas
{
"id": "recursion",
"name": "Recursion",
"type": "coding",
"stage": 7,
"desc": "caso base, caso recursivo, stack de llamadas, fibonacci, factorial",
"prereqs": ["functions_basic", "conditionals"],
},
{
"id": "higher_order",
"name": "Higher-order functions",
"type": "coding",
"stage": 7,
"desc": "map, filter, reduce, lambda, funciones como argumento",
"prereqs": ["functions_basic", "lists"],
},
{
"id": "generators",
"name": "Generators",
"type": "coding",
"stage": 7,
"desc": "yield, generadores, iteradores, lazy evaluation",
"prereqs": ["loops_for", "functions_basic"],
},
{
"id": "error_handling",
"name": "Error handling",
"type": "coding",
"stage": 7,
"desc": "try/except/finally, raise, excepciones custom",
"prereqs": ["functions_basic"],
},
# Nivel 8 β€” OOP
{
"id": "classes_basic",
"name": "Classes",
"type": "coding",
"stage": 8,
"desc": "class, __init__, self, atributos, mΓ©todos",
"prereqs": ["functions_basic", "dicts"],
},
{
"id": "inheritance",
"name": "Inheritance",
"type": "coding",
"stage": 8,
"desc": "herencia, super(), override, polimorfismo",
"prereqs": ["classes_basic"],
},
{
"id": "dunder_methods",
"name": "Dunder methods",
"type": "coding",
"stage": 8,
"desc": "__str__, __repr__, __len__, __add__, __eq__, __iter__",
"prereqs": ["classes_basic"],
},
# Nivel 9 β€” Avanzado
{
"id": "decorators",
"name": "Decorators",
"type": "coding",
"stage": 9,
"desc": "decoradores, functools.wraps, patrones de decorador",
"prereqs": ["higher_order"],
},
{
"id": "context_managers",
"name": "Context managers",
"type": "coding",
"stage": 9,
"desc": "with, __enter__/__exit__, contextlib",
"prereqs": ["classes_basic", "error_handling"],
},
{
"id": "algorithms",
"name": "Algorithms",
"type": "coding",
"stage": 9,
"desc": "sorting, searching, complejidad, divide and conquer",
"prereqs": ["recursion", "lists"],
},
{
"id": "file_io",
"name": "File I/O",
"type": "coding",
"stage": 9,
"desc": "open, read, write, with, json, csv",
"prereqs": ["error_handling", "strings"],
},
]
# Lookup rΓ‘pido por id
_CONCEPT_BY_ID = {c["id"]: c for c in CONCEPT_TREE}
# =============================================================================
# StudentProfile β€” tracking de quΓ© sabe el alumno
# =============================================================================
class StudentProfile:
"""Perfil adaptativo del alumno: trackea dominio por concepto."""
def __init__(self) -> None:
# concept_id β†’ {"correct": int, "total": int, "last_errors": [str]}
self.concepts: dict[str, dict] = defaultdict(
lambda: {"correct": 0, "total": 0, "last_errors": []}
)
self.lesson_count: int = 0
self.total_correct: int = 0
def record(self, concept_id: str, correct: bool, error_desc: str = "") -> None:
"""Registra un intento del alumno en un concepto."""
c = self.concepts[concept_id]
c["total"] += 1
if correct:
c["correct"] += 1
elif error_desc:
c["last_errors"] = (c["last_errors"] + [error_desc])[-3:]
self.lesson_count += 1
if correct:
self.total_correct += 1
def mastery(self, concept_id: str) -> float:
"""Porcentaje de dominio de un concepto (0.0 a 1.0)."""
c = self.concepts[concept_id]
if c["total"] == 0:
return 0.0
return c["correct"] / c["total"]
def is_mastered(self, concept_id: str, threshold: float = 0.7) -> bool:
"""Un concepto se domina si tiene >= threshold accuracy y >= 3 intentos."""
c = self.concepts[concept_id]
return c["total"] >= 3 and self.mastery(concept_id) >= threshold
def prereqs_met(self, concept_id: str) -> bool:
"""Verifica que los prerequisitos estΓ©n dominados (o no vistos aΓΊn)."""
concept = _CONCEPT_BY_ID.get(concept_id)
if not concept:
return True
for prereq in concept.get("prereqs", []):
# Prerequisito cumplido si: dominado O nunca intentado (permitir explorar)
c = self.concepts[prereq]
if c["total"] > 0 and not self.is_mastered(prereq):
return False
return True
def select_next_concept(self, max_drills: int = 6) -> str:
"""Elige el siguiente concepto a enseΓ±ar.
Prioridad:
1. Nuevos conceptos disponibles (prereqs cumplidos) β€” siempre introduce al menos uno nuevo
antes de volver a reforzar, una vez que el concepto anterior supera max_drills intentos.
2. Conceptos con intentos pero no dominados Y con < max_drills intentos (reforzar)
3. Nuevos conceptos cuyos prereqs estΓ‘n cumplidos (avanzar)
4. Conceptos dominados para repaso espaciado
max_drills: mΓ‘ximo de intentos seguidos en un concepto sin haberlo dominado
antes de forzar la introducciΓ³n de uno nuevo.
"""
# Separar conceptos que necesitan refuerzo y los que ya se han taladrado mucho
drillable = []
overdrilled = []
for concept in CONCEPT_TREE:
cid = concept["id"]
c = self.concepts[cid]
if c["total"] > 0 and not self.is_mastered(cid):
if c["total"] < max_drills:
drillable.append((cid, self.mastery(cid)))
else:
overdrilled.append((cid, self.mastery(cid)))
# Conceptos nuevos disponibles (prereqs cumplidos, no intentados)
available_new = [
concept["id"]
for concept in CONCEPT_TREE
if self.concepts[concept["id"]]["total"] == 0
and self.prereqs_met(concept["id"])
]
# Si hay conceptos taladrados en exceso (β‰₯ max_drills sin dominar)
# forzar introducciΓ³n de algo nuevo antes de volver a drillarlo
if overdrilled and available_new:
return available_new[0]
# Reforzar conceptos con pocos intentos (todavΓ­a ΓΊtil)
if drillable:
drillable.sort(key=lambda x: x[1])
return drillable[0][0]
# Avanzar a nuevo concepto aunque los anteriores no estΓ©n dominados
if available_new:
return available_new[0]
# Conceptos overdrilled: volver con ellos una vez agotados los nuevos
if overdrilled:
overdrilled.sort(key=lambda x: x[1])
return overdrilled[0][0]
# Repaso espaciado de conceptos dominados
mastered = [c["id"] for c in CONCEPT_TREE if self.is_mastered(c["id"])]
if mastered:
return random.choice(mastered)
# Fallback
return CONCEPT_TREE[0]["id"]
def summary(self) -> str:
"""Genera un resumen textual para el mentor."""
lines = [
f"Lessons completed: {self.lesson_count}, "
f"Overall accuracy: {self.total_correct}/{self.lesson_count} "
f"({100 * self.total_correct / max(1, self.lesson_count):.0f}%)"
]
mastered = []
struggling = []
untouched = []
for concept in CONCEPT_TREE:
cid = concept["id"]
c = self.concepts[cid]
if c["total"] == 0:
untouched.append(concept["name"])
elif self.is_mastered(cid):
mastered.append(concept["name"])
else:
pct = 100 * self.mastery(cid)
info = f"{concept['name']} ({pct:.0f}%)"
if c["last_errors"]:
info += f" β€” errors: {'; '.join(c['last_errors'][-2:])}"
struggling.append(info)
if mastered:
lines.append(f"Mastered: {', '.join(mastered)}")
if struggling:
lines.append(f"Struggling: {', '.join(struggling)}")
if untouched:
lines.append(f"Not yet taught: {', '.join(untouched[:5])}")
return "\n".join(lines)
# ── Utilidades ──────────────────────────────────────────────────────────
_LEVEL_MAP: dict[str, int] = {
# Etapa 0: presencia
"greeting": 0,
# Etapa 1: conceptos primitivos
"concept_number": 1,
"concept_sequence": 1,
"concept_word": 1,
"concept_true_false": 1,
# Etapa 2: lΓ³gica cotidiana
"concept_compare": 2,
"concept_if_then": 2,
"concept_repeat": 2,
# Etapa 3: procedimientos
"concept_steps": 3,
"concept_variable_box": 3,
"concept_function_machine": 3,
# Etapa 4: puente
"code_intro": 4,
"code_values": 4,
"code_true_false": 4,
"code_variables": 4,
"code_if": 4,
# Etapa 5+: cΓ³digo Python
"arithmetic": 5,
"variables_types": 5,
"conditionals": 5,
"strings": 5,
"functions_basic": 5,
"loops_for": 6,
"loops_while": 6,
"lists": 6,
"tuples_sets": 6,
"dicts": 6,
"recursion": 7,
"higher_order": 7,
"generators": 7,
"error_handling": 7,
"classes_basic": 8,
"inheritance": 8,
"dunder_methods": 8,
"decorators": 9,
"context_managers": 9,
"algorithms": 9,
"file_io": 9,
}
def concept_level(concept_id: str) -> int:
"""Mapea concept_id a nivel del curriculum (0-9).
0 = presencia, 1-3 = conceptual, 4 = puente, 5-9 = cΓ³digo Python.
"""
return _LEVEL_MAP.get(concept_id, 5)