evolutia 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,328 @@
1
+ """
2
+ Generador de archivos de examen en formato MyST/Markdown.
3
+ Crea la estructura completa de archivos para un examen.
4
+ """
5
+ import yaml
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional
8
+ from datetime import datetime
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ExamGenerator:
15
+ """Genera archivos de examen en formato MyST/Markdown."""
16
+
17
+ def __init__(self, base_path: Path):
18
+ """
19
+ Inicializa el generador.
20
+
21
+ Args:
22
+ base_path: Ruta base del proyecto
23
+ """
24
+ self.base_path = Path(base_path)
25
+
26
+ def generate_exam_frontmatter(self, exam_number: int, subject: str = "IF3602 - II semestre 2025",
27
+ tags: List[str] = None) -> str:
28
+ """
29
+ Genera el frontmatter YAML para un examen.
30
+
31
+ Args:
32
+ exam_number: Número del examen
33
+ subject: Asignatura
34
+ tags: Lista de tags agregados
35
+ """
36
+ if tags is None:
37
+ tags = []
38
+
39
+ frontmatter = {
40
+ 'title': f'Examen {exam_number}',
41
+ 'description': f'Examen {exam_number}',
42
+ 'short_title': f'Examen {exam_number}',
43
+ 'author': ' ',
44
+ 'tags': tags,
45
+ 'subject': subject,
46
+ 'date': datetime.now().strftime('%Y-%m-%d'),
47
+ 'downloads': []
48
+ }
49
+
50
+ # Convertir a YAML
51
+ yaml_str = yaml.dump(frontmatter, default_flow_style=False, allow_unicode=True, sort_keys=False)
52
+ return f"---\n{yaml_str}---\n\n"
53
+
54
+ def generate_instructions_block(self) -> str:
55
+ """
56
+ Genera el bloque de instrucciones del examen.
57
+
58
+ Returns:
59
+ String con el bloque de instrucciones en formato MyST
60
+ """
61
+ instructions = """:::{hint} Instrucciones
62
+ :class: dropdown
63
+
64
+ - Dispone de 2,0 horas para realizar el examen, **individualmente**.
65
+ - Debe mostrar la cédula de identidad o carnet universitario cuando se le solicite.
66
+ - La prueba tiene un valor de 100 puntos.
67
+ - Resuelva de forma razonada cada uno de los ejercicios.
68
+ - Use esquemas y dibujos si lo considera necesario.
69
+ - Debe incluir los cálculos y procedimientos que le llevan a su respuesta.
70
+ - Debe resolver el examen en un cuaderno de examen u hojas debidamente engrapadas, utilizando lapicero de tinta de color azul o negra.
71
+ - Si utiliza lápiz, corrector, lapicero de tinta roja o borrable, no se aceptarán reclamos.
72
+ - Sólo se evaluará lo escrito en el cuaderno de examen.
73
+ - Puede hacer uso únicamente de los materiales del curso; por medio de una computadora del aula C1-04. **No se permite el uso del teclado ni de herramientas de Inteligencia Artificial**.
74
+ - Una vez que el examen haya comenzado, quienes lleguen dentro de los primeros 30 minutos podrán realizarlo, pero solo dispondrán del tiempo restante.
75
+ - No se permitirá la salida del aula a ninguna persona estudiante durante los primeros 30 minutos de aplicación, salvo casos de fuerza mayor.
76
+ - Debe apagar y guardar su celular, tableta, reloj o cualquier otro dispositivo inteligente durante el desarrollo de la prueba.
77
+
78
+ :::
79
+ """
80
+ return instructions
81
+
82
+ def generate_exercise_section(self, exercise_num: int, exam_num: int,
83
+ points: int = 25) -> str:
84
+ """
85
+ Genera la sección de un ejercicio en el examen principal.
86
+
87
+ Args:
88
+ exercise_num: Número del ejercicio
89
+ exam_num: Número del examen
90
+ points: Puntos del ejercicio
91
+
92
+ Returns:
93
+ String con la sección del ejercicio
94
+ """
95
+ exercise_label = f"ex{exercise_num}_e{exam_num}"
96
+ solution_label = f"solucion-ex{exercise_num}_e{exam_num}"
97
+ exercise_file = f"./ex{exercise_num}_e{exam_num}.md"
98
+ solution_file = f"./solucion_ex{exercise_num}_e{exam_num}.md"
99
+
100
+ section = f"""## Ejercicio {exercise_num} [{points} puntos]
101
+ ````{{exercise}} {exercise_num}
102
+ :label: {exercise_label}
103
+
104
+ ```{{include}} {exercise_file}
105
+
106
+ ```
107
+
108
+ ````
109
+
110
+ ````{{solution}} {exercise_label}
111
+ :label: {solution_label}
112
+ :class: dropdown
113
+
114
+
115
+ ```{{include}} {solution_file}
116
+
117
+ ```
118
+ ````
119
+
120
+ """
121
+ return section
122
+
123
+ def generate_exercise_file(self, exercise_content: str, exercise_num: int,
124
+ exam_num: int, metadata: Dict = None) -> str:
125
+ """
126
+ Genera el contenido de un archivo de ejercicio individual.
127
+
128
+ Args:
129
+ exercise_content: Contenido del ejercicio
130
+ exercise_num: Número del ejercicio
131
+ exam_num: Número del examen
132
+ metadata: Metadatos opcionales (generator, model, date)
133
+
134
+ Returns:
135
+ Contenido del archivo
136
+ """
137
+ content = ""
138
+ if metadata:
139
+ frontmatter = {
140
+ 'generator': 'evolutia',
141
+ 'source': 'ai_variation',
142
+ 'date': datetime.now().isoformat()
143
+ }
144
+ frontmatter.update(metadata)
145
+ yaml_str = yaml.dump(frontmatter, default_flow_style=False, allow_unicode=True, sort_keys=False)
146
+ content += f"---\n{yaml_str}---\n\n"
147
+
148
+ content += exercise_content.strip() + "\n"
149
+ return content
150
+
151
+ def generate_solution_file(self, solution_content: str, exercise_num: int,
152
+ exam_num: int, metadata: Dict = None) -> str:
153
+ """
154
+ Genera el contenido de un archivo de solución individual.
155
+
156
+ Args:
157
+ solution_content: Contenido de la solución
158
+ exercise_num: Número del ejercicio
159
+ exam_num: Número del examen
160
+ metadata: Metadatos opcionales
161
+
162
+ Returns:
163
+ Contenido del archivo
164
+ """
165
+ content = ""
166
+ if metadata:
167
+ frontmatter = {
168
+ 'generator': 'evolutia',
169
+ 'source': 'ai_solution',
170
+ 'date': datetime.now().isoformat()
171
+ }
172
+ frontmatter.update(metadata)
173
+ yaml_str = yaml.dump(frontmatter, default_flow_style=False, allow_unicode=True, sort_keys=False)
174
+ content += f"---\n{yaml_str}---\n\n"
175
+
176
+ content += solution_content.strip() + "\n"
177
+ return content
178
+
179
+ def generate_exam(self, variations: List[Dict], exam_number: int,
180
+ output_dir: Path, subject: str = "IF3602 - II semestre 2025",
181
+ keywords: List[str] = None, metadata: Dict = None) -> bool:
182
+ """
183
+ Genera un examen completo con todas sus variaciones.
184
+
185
+ Args:
186
+ variations: Lista de variaciones generadas (cada una debe tener
187
+ 'variation_content' y opcionalmente 'variation_solution')
188
+ exam_number: Número del examen
189
+ output_dir: Directorio donde crear los archivos
190
+ subject: Asignatura
191
+ keywords: Palabras clave
192
+ metadata: Metadatos generales para incluir en ejercicios (ej: model)
193
+
194
+ Returns:
195
+ True si se generó exitosamente, False en caso contrario
196
+ """
197
+ try:
198
+ # Crear directorio si no existe
199
+ output_dir = Path(output_dir)
200
+ output_dir.mkdir(parents=True, exist_ok=True)
201
+
202
+ # Recolectar todos los tags de las variaciones
203
+ all_tags = set()
204
+ if keywords:
205
+ all_tags.update(keywords)
206
+
207
+ for variation in variations:
208
+ original_frontmatter = variation.get('original_frontmatter', {})
209
+ if 'tags' in original_frontmatter and original_frontmatter['tags']:
210
+ all_tags.update(original_frontmatter['tags'])
211
+
212
+ # Generar archivo principal del examen
213
+ exam_content = self.generate_exam_frontmatter(exam_number, subject, list(all_tags))
214
+ exam_content += self.generate_instructions_block()
215
+ exam_content += "\n"
216
+
217
+ # Agregar secciones de ejercicios
218
+ points_per_exercise = 100 // len(variations)
219
+ for i, variation in enumerate(variations, 1):
220
+ exam_content += self.generate_exercise_section(
221
+ i, exam_number, points_per_exercise
222
+ )
223
+
224
+ # Escribir archivo principal
225
+ exam_file = output_dir / f"examen{exam_number}.md"
226
+ with open(exam_file, 'w', encoding='utf-8') as f:
227
+ f.write(exam_content)
228
+
229
+ logger.info(f"Archivo principal creado: {exam_file}")
230
+
231
+ # Generar archivos individuales de ejercicios y soluciones
232
+ for i, variation in enumerate(variations, 1):
233
+ # Preparar metadatos específicos de esta variación
234
+ current_metadata = metadata.copy() if metadata else {}
235
+ original_frontmatter = variation.get('original_frontmatter', {})
236
+
237
+ # Agregar tags y subject originales si existen
238
+ if 'tags' in original_frontmatter:
239
+ current_metadata['tags'] = original_frontmatter['tags']
240
+ if 'subject' in original_frontmatter:
241
+ current_metadata['original_subject'] = original_frontmatter['subject']
242
+ if 'complexity' in original_frontmatter:
243
+ current_metadata['complexity'] = original_frontmatter['complexity']
244
+
245
+ # Add original label for traceability
246
+ if 'original_label' in variation and variation['original_label']:
247
+ current_metadata['based_on'] = variation['original_label']
248
+
249
+ # Add RAG references if available
250
+ if 'rag_references' in variation and variation['rag_references']:
251
+ current_metadata['rag_references'] = variation['rag_references']
252
+
253
+ # Archivo de ejercicio
254
+ exercise_content = variation.get('variation_content', '')
255
+ if exercise_content:
256
+ exercise_file = output_dir / f"ex{i}_e{exam_number}.md"
257
+ with open(exercise_file, 'w', encoding='utf-8') as f:
258
+ f.write(self.generate_exercise_file(
259
+ exercise_content, i, exam_number, current_metadata
260
+ ))
261
+ logger.info(f"Ejercicio creado: {exercise_file}")
262
+
263
+ # Archivo de solución
264
+ solution_content = variation.get('variation_solution', '')
265
+ if solution_content:
266
+ solution_file = output_dir / f"solucion_ex{i}_e{exam_number}.md"
267
+ with open(solution_file, 'w', encoding='utf-8') as f:
268
+ f.write(self.generate_solution_file(
269
+ solution_content, i, exam_number, current_metadata
270
+ ))
271
+ logger.info(f"Solución creada: {solution_file}")
272
+ else:
273
+ logger.warning(f"No hay solución para ejercicio {i}")
274
+
275
+ # Actualizar downloads en frontmatter
276
+ self._update_downloads_in_frontmatter(exam_file, exam_number, len(variations))
277
+
278
+ return True
279
+ except Exception as e:
280
+ logger.error(f"Error generando examen: {e}")
281
+ return False
282
+
283
+ def _update_downloads_in_frontmatter(self, exam_file: Path, exam_number: int,
284
+ num_exercises: int):
285
+ """
286
+ Actualiza la sección de downloads en el frontmatter del examen.
287
+
288
+ Args:
289
+ exam_file: Archivo del examen
290
+ exam_number: Número del examen
291
+ num_exercises: Número de ejercicios
292
+ """
293
+ try:
294
+ content = exam_file.read_text(encoding='utf-8')
295
+
296
+ # Extraer frontmatter
297
+ import re
298
+ frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
299
+ if not frontmatter_match:
300
+ return
301
+
302
+ frontmatter_str = frontmatter_match.group(1)
303
+ frontmatter = yaml.safe_load(frontmatter_str) or {}
304
+
305
+ # Crear lista de downloads
306
+ downloads = [
307
+ {'file': f'./examen{exam_number}.md', 'title': f'examen{exam_number}.md'},
308
+ {'file': f'./examen{exam_number}.pdf', 'title': f'examen{exam_number}.pdf'}
309
+ ]
310
+
311
+ for i in range(1, num_exercises + 1):
312
+ downloads.append({
313
+ 'file': f'./solucion_ex{i}_e{exam_number}.md',
314
+ 'title': f'solucion_ex{i}_e{exam_number}.md'
315
+ })
316
+
317
+ frontmatter['downloads'] = downloads
318
+
319
+ # Reemplazar frontmatter
320
+ new_frontmatter_str = yaml.dump(
321
+ frontmatter, default_flow_style=False, allow_unicode=True, sort_keys=False
322
+ )
323
+ new_content = f"---\n{new_frontmatter_str}---\n\n" + content[frontmatter_match.end():]
324
+
325
+ exam_file.write_text(new_content, encoding='utf-8')
326
+ except Exception as e:
327
+ logger.warning(f"No se pudo actualizar downloads: {e}")
328
+
@@ -0,0 +1,256 @@
1
+ """
2
+ Analizador de complejidad de ejercicios.
3
+ Identifica tipo, pasos, conceptos y variables de ejercicios.
4
+ """
5
+ import re
6
+ from typing import Dict, List, Set
7
+ from collections import Counter
8
+
9
+ try:
10
+ from utils.math_extractor import (
11
+ extract_math_expressions,
12
+ extract_variables,
13
+ count_math_operations,
14
+ estimate_complexity
15
+ )
16
+ except ImportError:
17
+ from .utils.math_extractor import (
18
+ extract_math_expressions,
19
+ extract_variables,
20
+ count_math_operations,
21
+ estimate_complexity
22
+ )
23
+
24
+
25
+ class ExerciseAnalyzer:
26
+ """Analiza la complejidad y estructura de ejercicios."""
27
+
28
+ # Palabras clave para identificación de tipo
29
+ DEMOSTRACION_KEYWORDS = [
30
+ 'demuestre', 'demuestre que', 'pruebe', 'verifique', 'muestre que'
31
+ ]
32
+
33
+ CALCULO_KEYWORDS = [
34
+ 'calcule', 'calcular', 'encuentre', 'determine', 'evalúe', 'evaluar'
35
+ ]
36
+
37
+ APLICACION_KEYWORDS = [
38
+ 'considere', 'suponga', 'modelo', 'sistema físico', 'aplicación',
39
+ 'dispositivo', 'campo', 'potencial'
40
+ ]
41
+
42
+ STEP_KEYWORDS = [
43
+ 'primero', 'luego', 'finalmente', 'ahora', 'a continuación',
44
+ 'por tanto', 'por lo tanto', 'en consecuencia', 'así',
45
+ 'por otro lado', 'además', 'también'
46
+ ]
47
+
48
+ # Patrones compilados para búsqueda eficiente
49
+ TYPE_PATTERNS = {
50
+ 'demostracion': re.compile('|'.join(map(re.escape, DEMOSTRACION_KEYWORDS)), re.IGNORECASE),
51
+ 'calculo': re.compile('|'.join(map(re.escape, CALCULO_KEYWORDS)), re.IGNORECASE),
52
+ 'aplicacion': re.compile('|'.join(map(re.escape, APLICACION_KEYWORDS)), re.IGNORECASE)
53
+ }
54
+
55
+ STEP_KEYWORDS_PATTERN = re.compile('|'.join(map(re.escape, STEP_KEYWORDS)), re.IGNORECASE)
56
+
57
+ # Conceptos matemáticos comunes
58
+ CONCEPT_PATTERNS = {
59
+ 'vector_operations': [
60
+ r'\\vec', r'\\cdot', r'\\times', r'\\nabla',
61
+ r'producto\s+(escalar|vectorial)', r'gradiente', r'divergencia', r'rotacional'
62
+ ],
63
+ 'coordinate_systems': [
64
+ r'coordenadas?\s+(cartesianas?|polares?|cilíndricas?|esféricas?|toroidales?)',
65
+ r'\\rho', r'\\theta', r'\\phi', r'\\hat\{e\}_'
66
+ ],
67
+ 'integrals': [
68
+ r'\\int', r'\\oint', r'integral', r'teorema\s+(de\s+)?(Green|Stokes|Gauss)',
69
+ r'divergencia', r'rotacional'
70
+ ],
71
+ 'differential_equations': [
72
+ r'\\frac\{d', r'\\partial', r'ecuaci[óo]n\s+diferencial',
73
+ r'EDP', r'EDO'
74
+ ],
75
+ 'linear_algebra': [
76
+ r'\\begin\{matrix\}', r'\\begin\{pmatrix\}', r'\\begin\{bmatrix\}',
77
+ r'matriz', r'\\mathbf', r'autovalor', r'autovector'
78
+ ],
79
+ 'complex_numbers': [
80
+ r'\\mathbb\{C\}', r'z\s*=', r'n[úu]mero\s+complejo',
81
+ r'\\Re', r'\\Im', r'\\arg'
82
+ ],
83
+ 'series_expansions': [
84
+ r'\\sum', r'serie', r'expansi[óo]n', r'Fourier',
85
+ r'Taylor', r'\\sum_\{n=0\}'
86
+ ]
87
+ }
88
+
89
+ def __init__(self):
90
+ """Inicializa el analizador."""
91
+ pass
92
+
93
+ def identify_exercise_type(self, content: str) -> str:
94
+ """
95
+ Identifica el tipo de ejercicio.
96
+
97
+ Args:
98
+ content: Contenido del ejercicio
99
+
100
+ Returns:
101
+ Tipo de ejercicio: 'demostracion', 'calculo', 'aplicacion', 'mixto'
102
+ """
103
+ # Búsqueda optimizada con evaluación perezosa (short-circuit)
104
+ # Verificamos demostración primero ya que es determinante para 'mixto'
105
+ if self.TYPE_PATTERNS['demostracion'].search(content):
106
+ # Si es demostración, buscamos otros tipos para ver si es mixto
107
+ # Basta con encontrar uno de los dos para que sea mixto
108
+ if (self.TYPE_PATTERNS['calculo'].search(content) or
109
+ self.TYPE_PATTERNS['aplicacion'].search(content)):
110
+ return 'mixto'
111
+ return 'demostracion'
112
+
113
+ # Si no es demostración, buscamos cálculo
114
+ if self.TYPE_PATTERNS['calculo'].search(content):
115
+ return 'calculo'
116
+
117
+ # Finalmente aplicación
118
+ if self.TYPE_PATTERNS['aplicacion'].search(content):
119
+ return 'aplicacion'
120
+
121
+ return 'calculo' # Por defecto
122
+
123
+ def count_solution_steps(self, solution_content: str) -> int:
124
+ """
125
+ Cuenta el número de pasos en una solución.
126
+
127
+ Busca indicadores de pasos como:
128
+ - Numeración (1., 2., etc.)
129
+ - Palabras clave (Primero, Luego, Finalmente, etc.)
130
+ - Bloques de ecuaciones separados
131
+
132
+ Args:
133
+ solution_content: Contenido de la solución
134
+
135
+ Returns:
136
+ Número estimado de pasos
137
+ """
138
+ if not solution_content:
139
+ return 0
140
+
141
+ # Contar numeración explícita
142
+ numbered_steps = len(re.findall(r'^\s*\d+[\.\)]\s+', solution_content, re.MULTILINE))
143
+
144
+ # Contar palabras clave de pasos
145
+ keyword_steps = len(self.STEP_KEYWORDS_PATTERN.findall(solution_content))
146
+
147
+ # Contar bloques de ecuaciones (align, equation)
148
+ equation_blocks = len(re.findall(
149
+ r'\\begin\{(align|equation|aligned|eqnarray)\}',
150
+ solution_content
151
+ ))
152
+
153
+ # Estimar pasos basado en separadores
154
+ separators = len(re.findall(r'\n\n+', solution_content))
155
+
156
+ # Tomar el máximo de los métodos
157
+ estimated_steps = max(
158
+ numbered_steps,
159
+ keyword_steps // 2, # Dividir porque pueden repetirse
160
+ equation_blocks,
161
+ separators // 2
162
+ )
163
+
164
+ return max(1, estimated_steps) # Mínimo 1 paso
165
+
166
+ def identify_concepts(self, content: str) -> Set[str]:
167
+ """
168
+ Identifica conceptos matemáticos presentes en el contenido.
169
+
170
+ Args:
171
+ content: Contenido a analizar
172
+
173
+ Returns:
174
+ Conjunto de conceptos identificados
175
+ """
176
+ concepts = set()
177
+
178
+ for concept_name, patterns in self.CONCEPT_PATTERNS.items():
179
+ for pattern in patterns:
180
+ if re.search(pattern, content, re.IGNORECASE):
181
+ concepts.add(concept_name)
182
+ break
183
+
184
+ return concepts
185
+
186
+ def analyze(self, exercise: Dict) -> Dict:
187
+ """
188
+ Analiza un ejercicio completo y retorna metadatos de complejidad.
189
+
190
+ Args:
191
+ exercise: Diccionario con información del ejercicio
192
+ - 'content': Contenido del ejercicio
193
+ - 'solution': Contenido de la solución (opcional)
194
+
195
+ Returns:
196
+ Diccionario con análisis de complejidad
197
+ """
198
+ content = exercise.get('content', '')
199
+ solution = exercise.get('solution', '')
200
+
201
+ # Extraer expresiones matemáticas
202
+ math_expressions = extract_math_expressions(content)
203
+ if solution:
204
+ math_expressions.extend(extract_math_expressions(solution))
205
+
206
+ # Extraer variables
207
+ variables = extract_variables(math_expressions)
208
+
209
+ # Identificar tipo
210
+ exercise_type = self.identify_exercise_type(content)
211
+
212
+ # Contar pasos en solución
213
+ solution_steps = self.count_solution_steps(solution) if solution else 0
214
+
215
+ # Identificar conceptos
216
+ all_content = content + '\n' + (solution or '')
217
+ concepts = self.identify_concepts(all_content)
218
+
219
+ # Calcular complejidad matemática
220
+ math_complexity = estimate_complexity(math_expressions)
221
+
222
+ # Contar operaciones
223
+ total_operations = {
224
+ 'integrals': 0,
225
+ 'derivatives': 0,
226
+ 'sums': 0,
227
+ 'vectors': 0,
228
+ 'matrices': 0,
229
+ 'functions': 0
230
+ }
231
+ for expr in math_expressions:
232
+ ops = count_math_operations(expr)
233
+ for key in total_operations:
234
+ total_operations[key] += ops[key]
235
+
236
+ # Calcular complejidad total
237
+ total_complexity = (
238
+ math_complexity +
239
+ solution_steps * 2.0 +
240
+ len(variables) * 0.5 +
241
+ len(concepts) * 1.5 +
242
+ sum(total_operations.values()) * 0.5
243
+ )
244
+
245
+ return {
246
+ 'type': exercise_type,
247
+ 'solution_steps': solution_steps,
248
+ 'variables': list(variables),
249
+ 'num_variables': len(variables),
250
+ 'concepts': list(concepts),
251
+ 'num_concepts': len(concepts),
252
+ 'math_complexity': math_complexity,
253
+ 'operations': total_operations,
254
+ 'total_complexity': total_complexity,
255
+ 'num_math_expressions': len(math_expressions)
256
+ }