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.
- evolutia/__init__.py +5 -0
- evolutia/complexity_validator.py +179 -0
- evolutia/config_manager.py +208 -0
- evolutia/evolutia_engine.py +284 -0
- evolutia/exam_generator.py +328 -0
- evolutia/exercise_analyzer.py +256 -0
- evolutia/llm_providers.py +217 -0
- evolutia/material_extractor.py +237 -0
- evolutia/rag/__init__.py +6 -0
- evolutia/rag/consistency_validator.py +200 -0
- evolutia/rag/context_enricher.py +285 -0
- evolutia/rag/enhanced_variation_generator.py +349 -0
- evolutia/rag/rag_indexer.py +424 -0
- evolutia/rag/rag_manager.py +221 -0
- evolutia/rag/rag_retriever.py +366 -0
- evolutia/utils/__init__.py +4 -0
- evolutia/utils/json_parser.py +69 -0
- evolutia/utils/markdown_parser.py +160 -0
- evolutia/utils/math_extractor.py +144 -0
- evolutia/variation_generator.py +97 -0
- evolutia-0.1.0.dist-info/METADATA +723 -0
- evolutia-0.1.0.dist-info/RECORD +27 -0
- evolutia-0.1.0.dist-info/WHEEL +5 -0
- evolutia-0.1.0.dist-info/entry_points.txt +2 -0
- evolutia-0.1.0.dist-info/licenses/LICENSE +201 -0
- evolutia-0.1.0.dist-info/top_level.txt +2 -0
- evolutia_cli.py +160 -0
|
@@ -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
|
+
}
|