| """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."""
|
|
|
|
|
| checkpoint_in: str = "checkpoints/v3_ghidra_v9.pt"
|
| checkpoint_out: str = "checkpoints/v3_classroom.pt"
|
| device: str = "auto"
|
|
|
|
|
| teacher_backend: str = "github"
|
| teacher_model: str = "openai/gpt-4o-mini"
|
| api_key: str = ""
|
|
|
|
|
| lr_base: float = 5e-6
|
| lr_llaves_mult: float = 0.01
|
| lr_attn_mult: float = 0.1
|
| lr_embed_mult: float = 0.1
|
| lr_ffn_mult: float = 1.0
|
|
|
|
|
| ewc_lambda: float = 500.0
|
| ewc_samples: int = 200
|
|
|
|
|
| replay_size: int = 100
|
| replay_ratio: float = 0.5
|
|
|
|
|
| start_level: int = 1
|
| advance_threshold: float = 0.7
|
| window_size: int = 10
|
|
|
|
|
| max_lessons: int = 200
|
| guardar_cada: int = 20
|
| seq_len: int = 256
|
|
|
|
|
| bio_enabled: bool = True
|
| sleep_every: int = 15
|
| prune_every: int = 30
|
|
|
|
|
| port: int = 8888
|
|
|
|
|
| record: bool = True
|
|
|
|
|
| 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.",
|
| ],
|
| },
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| CONCEPT_TREE: list[dict] = [
|
|
|
|
|
|
|
|
|
| {
|
| "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": [],
|
| },
|
|
|
|
|
|
|
|
|
| {
|
| "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"],
|
| },
|
|
|
|
|
|
|
|
|
| {
|
| "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"],
|
| },
|
|
|
|
|
|
|
|
|
| {
|
| "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"],
|
| },
|
|
|
|
|
|
|
|
|
| {
|
| "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"],
|
| },
|
|
|
|
|
|
|
|
|
| {
|
| "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"],
|
| },
|
|
|
| {
|
| "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"],
|
| },
|
|
|
| {
|
| "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"],
|
| },
|
|
|
| {
|
| "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"],
|
| },
|
|
|
| {
|
| "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"],
|
| },
|
| ]
|
|
|
|
|
| _CONCEPT_BY_ID = {c["id"]: c for c in CONCEPT_TREE}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class StudentProfile:
|
| """Perfil adaptativo del alumno: trackea dominio por concepto."""
|
|
|
| def __init__(self) -> None:
|
|
|
| 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", []):
|
|
|
| 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.
|
| """
|
|
|
| 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)))
|
|
|
|
|
| available_new = [
|
| concept["id"]
|
| for concept in CONCEPT_TREE
|
| if self.concepts[concept["id"]]["total"] == 0
|
| and self.prereqs_met(concept["id"])
|
| ]
|
|
|
|
|
|
|
| if overdrilled and available_new:
|
| return available_new[0]
|
|
|
|
|
| if drillable:
|
| drillable.sort(key=lambda x: x[1])
|
| return drillable[0][0]
|
|
|
|
|
| if available_new:
|
| return available_new[0]
|
|
|
|
|
| if overdrilled:
|
| overdrilled.sort(key=lambda x: x[1])
|
| return overdrilled[0][0]
|
|
|
|
|
| mastered = [c["id"] for c in CONCEPT_TREE if self.is_mastered(c["id"])]
|
| if mastered:
|
| return random.choice(mastered)
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
| _LEVEL_MAP: dict[str, int] = {
|
|
|
| "greeting": 0,
|
|
|
| "concept_number": 1,
|
| "concept_sequence": 1,
|
| "concept_word": 1,
|
| "concept_true_false": 1,
|
|
|
| "concept_compare": 2,
|
| "concept_if_then": 2,
|
| "concept_repeat": 2,
|
|
|
| "concept_steps": 3,
|
| "concept_variable_box": 3,
|
| "concept_function_machine": 3,
|
|
|
| "code_intro": 4,
|
| "code_values": 4,
|
| "code_true_false": 4,
|
| "code_variables": 4,
|
| "code_if": 4,
|
|
|
| "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)
|
|
|