evolutia 0.1.1__py3-none-any.whl → 0.1.3__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 +9 -0
- evolutia/async_llm_providers.py +157 -0
- evolutia/cache/__init__.py +9 -0
- evolutia/cache/exercise_cache.py +226 -0
- evolutia/cache/llm_cache.py +487 -0
- evolutia/complexity_validator.py +33 -31
- evolutia/config_manager.py +53 -40
- evolutia/evolutia_engine.py +341 -66
- evolutia/exam_generator.py +44 -43
- evolutia/exceptions.py +38 -0
- evolutia/exercise_analyzer.py +42 -59
- evolutia/imports.py +175 -0
- evolutia/llm_providers.py +223 -61
- evolutia/material_extractor.py +166 -88
- evolutia/rag/rag_indexer.py +107 -90
- evolutia/rag/rag_retriever.py +130 -103
- evolutia/retry_utils.py +280 -0
- evolutia/utils/json_parser.py +29 -19
- evolutia/utils/markdown_parser.py +185 -159
- evolutia/utils/math_extractor.py +153 -144
- evolutia/validation/__init__.py +1 -0
- evolutia/validation/args_validator.py +253 -0
- evolutia/validation/config_validator.py +502 -0
- evolutia/variation_generator.py +82 -70
- evolutia-0.1.3.dist-info/METADATA +536 -0
- evolutia-0.1.3.dist-info/RECORD +37 -0
- {evolutia-0.1.1.dist-info → evolutia-0.1.3.dist-info}/WHEEL +1 -1
- evolutia_cli.py +22 -9
- evolutia-0.1.1.dist-info/METADATA +0 -221
- evolutia-0.1.1.dist-info/RECORD +0 -27
- {evolutia-0.1.1.dist-info → evolutia-0.1.3.dist-info}/entry_points.txt +0 -0
- {evolutia-0.1.1.dist-info → evolutia-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {evolutia-0.1.1.dist-info → evolutia-0.1.3.dist-info}/top_level.txt +0 -0
evolutia/evolutia_engine.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Motor principal de EvoluIA.
|
|
3
|
-
Encapsula la lógica de orquestación, extracción, análisis y generación paralela.
|
|
4
|
-
"""
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from
|
|
1
|
+
"""
|
|
2
|
+
Motor principal de EvoluIA.
|
|
3
|
+
Encapsula la lógica de orquestación, extracción, análisis y generación paralela.
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import random
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Dict, Optional, Tuple, Any, Union
|
|
13
|
+
from tqdm import tqdm
|
|
12
14
|
|
|
13
15
|
# Imports from internal modules
|
|
14
16
|
from .material_extractor import MaterialExtractor
|
|
@@ -20,9 +22,9 @@ from .config_manager import ConfigManager
|
|
|
20
22
|
|
|
21
23
|
# Conditional RAG imports
|
|
22
24
|
try:
|
|
23
|
-
from rag.rag_manager import RAGManager
|
|
24
|
-
from rag.enhanced_variation_generator import EnhancedVariationGenerator
|
|
25
|
-
from rag.consistency_validator import ConsistencyValidator
|
|
25
|
+
from .rag.rag_manager import RAGManager
|
|
26
|
+
from .rag.enhanced_variation_generator import EnhancedVariationGenerator
|
|
27
|
+
from .rag.consistency_validator import ConsistencyValidator
|
|
26
28
|
RAG_AVAILABLE = True
|
|
27
29
|
except ImportError:
|
|
28
30
|
RAG_AVAILABLE = False
|
|
@@ -34,28 +36,35 @@ class EvolutiaEngine:
|
|
|
34
36
|
Motor central que coordina el flujo de trabajo de EvolutIA.
|
|
35
37
|
"""
|
|
36
38
|
|
|
37
|
-
def __init__(self, base_path: Path, config_path: Optional[Path] = None):
|
|
38
|
-
self.base_path = base_path
|
|
39
|
-
self.config_path = config_path
|
|
40
|
-
self.rag_manager = None
|
|
39
|
+
def __init__(self, base_path: Union[Path, str], config_path: Optional[Union[Path, str]] = None):
|
|
40
|
+
self.base_path = Path(base_path)
|
|
41
|
+
self.config_path = Path(config_path) if config_path else None
|
|
42
|
+
self.rag_manager = None
|
|
41
43
|
|
|
42
44
|
# Load configuration manager
|
|
43
45
|
self.config_manager = ConfigManager(base_path, config_path)
|
|
44
46
|
self.full_config = self.config_manager.load_current_config()
|
|
45
47
|
|
|
46
|
-
def initialize_rag(self, force_reindex: bool = False) -> bool:
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
def initialize_rag(self, force_reindex: bool = False) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Inicializa el subsistema RAG si está disponible.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True si la inicialización fue exitosa
|
|
54
|
+
False si no se pudo inicializar (RAG no disponible o error)
|
|
55
|
+
"""
|
|
56
|
+
if not RAG_AVAILABLE:
|
|
57
|
+
logger.error("[EvolutiaEngine] RAG solicitado pero no disponible. Instala dependencias con: pip install -e '.[rag]'")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
self.rag_manager = RAGManager(config_path=self.config_path, base_path=self.base_path)
|
|
62
|
+
self.rag_manager.initialize(force_reindex=force_reindex)
|
|
63
|
+
logger.info("[EvolutiaEngine] Subsistema RAG inicializado correctamente")
|
|
64
|
+
return True
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"[EvolutiaEngine] Error inicializando RAG (force_reindex={force_reindex}): {e}")
|
|
67
|
+
return False
|
|
59
68
|
|
|
60
69
|
def get_api_config(self, provider: str) -> Dict[str, Any]:
|
|
61
70
|
"""Obtiene la configuración específica para una API."""
|
|
@@ -120,8 +129,15 @@ class EvolutiaEngine:
|
|
|
120
129
|
exercises_with_analysis.sort(key=lambda x: x[1]['total_complexity'], reverse=True)
|
|
121
130
|
return exercises_with_analysis
|
|
122
131
|
|
|
123
|
-
def _generate_single_variation(
|
|
124
|
-
|
|
132
|
+
def _generate_single_variation(
|
|
133
|
+
self,
|
|
134
|
+
generator: Union['VariationGenerator', 'EnhancedVariationGenerator'],
|
|
135
|
+
validator: Union['ComplexityValidator', 'ConsistencyValidator'],
|
|
136
|
+
exercise_base: Dict,
|
|
137
|
+
analysis: Dict,
|
|
138
|
+
args: argparse.Namespace
|
|
139
|
+
) -> Optional[Dict]:
|
|
140
|
+
"""Helper para generar una única variación (thread-safe logic)."""
|
|
125
141
|
attempt_count = 0
|
|
126
142
|
while attempt_count < 3:
|
|
127
143
|
try:
|
|
@@ -159,32 +175,48 @@ class EvolutiaEngine:
|
|
|
159
175
|
if is_valid:
|
|
160
176
|
return variation
|
|
161
177
|
|
|
162
|
-
except Exception as e:
|
|
163
|
-
logger.error(f"Error en hilo de generación: {e}")
|
|
164
|
-
|
|
165
|
-
attempt_count += 1
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"[EvolutiaEngine] Error en hilo de generación (intento {attempt_count + 1}/3): {e}")
|
|
180
|
+
|
|
181
|
+
attempt_count += 1
|
|
182
|
+
logger.debug(f"[EvolutiaEngine] Generación falló después de {attempt_count} intentos")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
def _generate_creation_mode(
|
|
186
|
+
self,
|
|
187
|
+
generator: Union['VariationGenerator', 'EnhancedVariationGenerator'],
|
|
188
|
+
topic: str,
|
|
189
|
+
tags: List[str],
|
|
190
|
+
complexity: str,
|
|
191
|
+
ex_type: str
|
|
192
|
+
) -> Optional[Dict]:
|
|
193
|
+
"""
|
|
194
|
+
Helper para modo creación.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Diccionario con el ejercicio generado o None si hubo error
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
logger.debug(f"[EvolutiaEngine] Creando nuevo ejercicio: topic={topic}, tags={tags}, complexity={complexity}")
|
|
201
|
+
return generator.generate_new_exercise_from_topic(
|
|
202
|
+
topic,
|
|
203
|
+
tags,
|
|
204
|
+
difficulty=complexity,
|
|
205
|
+
exercise_type=ex_type
|
|
206
|
+
)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"[EvolutiaEngine] Error en creación de ejercicio nuevo (topic={topic}): {e}")
|
|
209
|
+
return None
|
|
180
210
|
|
|
181
|
-
def generate_variations_parallel(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
"""
|
|
211
|
+
def generate_variations_parallel(
|
|
212
|
+
self,
|
|
213
|
+
selected_exercises: List[Tuple[Dict, Dict]],
|
|
214
|
+
args: argparse.Namespace,
|
|
215
|
+
max_workers: int = 5
|
|
216
|
+
) -> List[Dict]:
|
|
217
|
+
"""
|
|
218
|
+
Paso 4: Genera variaciones en paralelo.
|
|
219
|
+
"""
|
|
188
220
|
logger.info(f"Paso 4: Generando variaciones en paralelo (Workers: {max_workers})...")
|
|
189
221
|
|
|
190
222
|
# Setup Generator
|
|
@@ -200,10 +232,15 @@ class EvolutiaEngine:
|
|
|
200
232
|
|
|
201
233
|
# Configure model
|
|
202
234
|
if args.api == 'local':
|
|
203
|
-
generator.base_url = api_config.get('base_url', "http://localhost:11434/v1")
|
|
204
|
-
generator.local_model = api_config.get('model', "llama3")
|
|
205
|
-
elif args.api
|
|
206
|
-
|
|
235
|
+
generator.base_url = args.base_url or api_config.get('base_url', "http://localhost:11434/v1")
|
|
236
|
+
generator.local_model = args.model or api_config.get('model', "llama3")
|
|
237
|
+
elif args.api == 'generic':
|
|
238
|
+
generator.base_url = args.base_url or api_config.get('base_url')
|
|
239
|
+
generator.model_name = args.model or api_config.get('model')
|
|
240
|
+
elif args.api in ['openai', 'anthropic', 'deepseek', 'gemini']:
|
|
241
|
+
if args.model:
|
|
242
|
+
generator.model_name = args.model
|
|
243
|
+
elif 'model' in api_config:
|
|
207
244
|
generator.model_name = api_config['model']
|
|
208
245
|
|
|
209
246
|
# Determine tasks based on mode
|
|
@@ -257,11 +294,249 @@ class EvolutiaEngine:
|
|
|
257
294
|
except Exception as e:
|
|
258
295
|
logger.error(f"Excepción no manejada en worker: {e}")
|
|
259
296
|
|
|
260
|
-
logger.info(f"Generación completada. {len(valid_variations)} variaciones exitosas.")
|
|
261
|
-
return valid_variations
|
|
297
|
+
logger.info(f"Generación completada. {len(valid_variations)} variaciones exitosas.")
|
|
298
|
+
return valid_variations
|
|
299
|
+
|
|
300
|
+
async def _generate_single_variation_async(
|
|
301
|
+
self,
|
|
302
|
+
generator: Union['VariationGenerator', 'EnhancedVariationGenerator'],
|
|
303
|
+
validator: Union['ComplexityValidator', 'ConsistencyValidator'],
|
|
304
|
+
exercise_base: Dict,
|
|
305
|
+
analysis: Dict,
|
|
306
|
+
args: argparse.Namespace,
|
|
307
|
+
semaphore: asyncio.Semaphore
|
|
308
|
+
) -> Optional[Dict]:
|
|
309
|
+
"""
|
|
310
|
+
Helper asíncrono para generar una única variación.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
generator: Generador de variaciones
|
|
314
|
+
validator: Validador de complejidad
|
|
315
|
+
exercise_base: Ejercicio base
|
|
316
|
+
analysis: Análisis del ejercicio
|
|
317
|
+
args: Argumentos CLI
|
|
318
|
+
semaphore: Semaphore para limitar concurrencia
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Variación generada o None
|
|
322
|
+
"""
|
|
323
|
+
async with semaphore:
|
|
324
|
+
attempt_count = 0
|
|
325
|
+
while attempt_count < 3:
|
|
326
|
+
try:
|
|
327
|
+
# Ejecutar generación sincrónica en un thread
|
|
328
|
+
loop = asyncio.get_event_loop()
|
|
329
|
+
if args.type == 'multiple_choice':
|
|
330
|
+
variation = await loop.run_in_executor(
|
|
331
|
+
None,
|
|
332
|
+
generator.generate_variation,
|
|
333
|
+
exercise_base,
|
|
334
|
+
analysis,
|
|
335
|
+
args.type
|
|
336
|
+
)
|
|
337
|
+
elif not args.no_generar_soluciones:
|
|
338
|
+
variation = await loop.run_in_executor(
|
|
339
|
+
None,
|
|
340
|
+
generator.generate_variation_with_solution,
|
|
341
|
+
exercise_base,
|
|
342
|
+
analysis
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
variation = await loop.run_in_executor(
|
|
346
|
+
None,
|
|
347
|
+
generator.generate_variation,
|
|
348
|
+
exercise_base,
|
|
349
|
+
analysis,
|
|
350
|
+
args.type
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if not variation:
|
|
354
|
+
attempt_count += 1
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
# Validar
|
|
358
|
+
validation = await loop.run_in_executor(
|
|
359
|
+
None,
|
|
360
|
+
validator.validate,
|
|
361
|
+
exercise_base,
|
|
362
|
+
analysis,
|
|
363
|
+
variation
|
|
364
|
+
)
|
|
365
|
+
is_valid = validation['is_valid']
|
|
366
|
+
|
|
367
|
+
if is_valid:
|
|
368
|
+
return variation
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"[EvolutiaEngine] Error en async worker (intento {attempt_count + 1}/3): {e}")
|
|
372
|
+
|
|
373
|
+
attempt_count += 1
|
|
374
|
+
logger.debug(f"[EvolutiaEngine] Generación async falló después de {attempt_count} intentos")
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
async def _generate_creation_mode_async(
|
|
378
|
+
self,
|
|
379
|
+
generator: Union['VariationGenerator', 'EnhancedVariationGenerator'],
|
|
380
|
+
topic: str,
|
|
381
|
+
tags: List[str],
|
|
382
|
+
complexity: str,
|
|
383
|
+
ex_type: str,
|
|
384
|
+
semaphore: asyncio.Semaphore
|
|
385
|
+
) -> Optional[Dict]:
|
|
386
|
+
"""
|
|
387
|
+
Helper asíncrono para modo creación.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Diccionario con el ejercicio generado o None si hubo error
|
|
391
|
+
"""
|
|
392
|
+
async with semaphore:
|
|
393
|
+
try:
|
|
394
|
+
logger.debug(f"[EvolutiaEngine] Creando nuevo ejercicio async: topic={topic}, tags={tags}")
|
|
395
|
+
loop = asyncio.get_event_loop()
|
|
396
|
+
return await loop.run_in_executor(
|
|
397
|
+
None,
|
|
398
|
+
generator.generate_new_exercise_from_topic,
|
|
399
|
+
topic,
|
|
400
|
+
tags,
|
|
401
|
+
complexity,
|
|
402
|
+
ex_type
|
|
403
|
+
)
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.error(f"[EvolutiaEngine] Error en async creación (topic={topic}): {e}")
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
def generate_variations_async(
|
|
409
|
+
self,
|
|
410
|
+
selected_exercises: List[Tuple[Dict, Dict]],
|
|
411
|
+
args: argparse.Namespace,
|
|
412
|
+
max_workers: int = 5
|
|
413
|
+
) -> List[Dict]:
|
|
414
|
+
"""
|
|
415
|
+
Generación de variaciones usando asyncio (async/await).
|
|
416
|
+
|
|
417
|
+
Más eficiente que ThreadPoolExecutor para operaciones I/O-bound como llamadas a LLM APIs.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
selected_exercises: Lista de ejercicios seleccionados con análisis
|
|
421
|
+
args: Argumentos CLI
|
|
422
|
+
max_workers: Número máximo de workers concurrentes
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Lista de variaciones generadas exitosamente
|
|
426
|
+
"""
|
|
427
|
+
logger.info(f"Paso 4: Generando variaciones con asyncio (max_workers={max_workers})...")
|
|
428
|
+
|
|
429
|
+
# Setup Generator
|
|
430
|
+
api_config = self.get_api_config(args.api)
|
|
431
|
+
|
|
432
|
+
if (args.use_rag and self.rag_manager) or args.mode == 'creation':
|
|
433
|
+
retriever = self.rag_manager.get_retriever() if (args.use_rag and self.rag_manager) else None
|
|
434
|
+
generator = EnhancedVariationGenerator(api_provider=args.api, retriever=retriever)
|
|
435
|
+
validator = ConsistencyValidator(retriever=retriever) if retriever else ComplexityValidator()
|
|
436
|
+
else:
|
|
437
|
+
generator = VariationGenerator(api_provider=args.api)
|
|
438
|
+
validator = ComplexityValidator()
|
|
439
|
+
|
|
440
|
+
# Configure model
|
|
441
|
+
if args.api == 'local':
|
|
442
|
+
generator.base_url = args.base_url or api_config.get('base_url', "http://localhost:11434/v1")
|
|
443
|
+
generator.local_model = args.model or api_config.get('model', "llama3")
|
|
444
|
+
elif args.api == 'generic':
|
|
445
|
+
generator.base_url = args.base_url or api_config.get('base_url')
|
|
446
|
+
generator.model_name = args.model or api_config.get('model')
|
|
447
|
+
elif args.api in ['openai', 'anthropic', 'deepseek', 'gemini']:
|
|
448
|
+
if args.model:
|
|
449
|
+
generator.model_name = args.model
|
|
450
|
+
elif 'model' in api_config:
|
|
451
|
+
generator.model_name = api_config['model']
|
|
452
|
+
|
|
453
|
+
# Determine tasks
|
|
454
|
+
tasks = []
|
|
455
|
+
|
|
456
|
+
if args.mode == 'creation':
|
|
457
|
+
for i in range(args.num_ejercicios):
|
|
458
|
+
current_topic = args.tema[i % len(args.tema)]
|
|
459
|
+
current_tags = [args.tags[i % len(args.tags)]] if args.tags else [current_topic]
|
|
460
|
+
tasks.append((
|
|
461
|
+
'creation',
|
|
462
|
+
generator,
|
|
463
|
+
current_topic,
|
|
464
|
+
current_tags,
|
|
465
|
+
args.complejidad,
|
|
466
|
+
args.type
|
|
467
|
+
))
|
|
468
|
+
else:
|
|
469
|
+
if args.label:
|
|
470
|
+
target_exercises = list(selected_exercises)
|
|
471
|
+
else:
|
|
472
|
+
target_exercises = []
|
|
473
|
+
candidates = selected_exercises[:max(5, len(selected_exercises)//2)]
|
|
474
|
+
for _ in range(args.num_ejercicios):
|
|
475
|
+
if candidates:
|
|
476
|
+
target_exercises.append(random.choice(candidates))
|
|
477
|
+
|
|
478
|
+
for ex_base, analysis in target_exercises:
|
|
479
|
+
tasks.append((
|
|
480
|
+
'variation',
|
|
481
|
+
generator,
|
|
482
|
+
validator,
|
|
483
|
+
ex_base,
|
|
484
|
+
analysis,
|
|
485
|
+
args
|
|
486
|
+
))
|
|
487
|
+
|
|
488
|
+
# Create semaphore to limit concurrency
|
|
489
|
+
semaphore = asyncio.Semaphore(max_workers)
|
|
490
|
+
|
|
491
|
+
# Create async tasks
|
|
492
|
+
async_tasks = []
|
|
493
|
+
for task_info in tasks:
|
|
494
|
+
if task_info[0] == 'creation':
|
|
495
|
+
async_tasks.append(self._generate_creation_mode_async(
|
|
496
|
+
task_info[1], # generator
|
|
497
|
+
task_info[2], # topic
|
|
498
|
+
task_info[3], # tags
|
|
499
|
+
task_info[4], # complexity
|
|
500
|
+
task_info[5], # ex_type
|
|
501
|
+
semaphore
|
|
502
|
+
))
|
|
503
|
+
else:
|
|
504
|
+
async_tasks.append(self._generate_single_variation_async(
|
|
505
|
+
task_info[1], # generator
|
|
506
|
+
task_info[2], # validator
|
|
507
|
+
task_info[3], # ex_base
|
|
508
|
+
task_info[4], # analysis
|
|
509
|
+
task_info[5], # args
|
|
510
|
+
semaphore
|
|
511
|
+
))
|
|
512
|
+
|
|
513
|
+
# Execute all tasks concurrently
|
|
514
|
+
try:
|
|
515
|
+
loop = asyncio.get_event_loop()
|
|
516
|
+
except RuntimeError:
|
|
517
|
+
loop = asyncio.new_event_loop()
|
|
518
|
+
asyncio.set_event_loop(loop)
|
|
519
|
+
|
|
520
|
+
valid_variations = []
|
|
521
|
+
for coro in tqdm(asyncio.as_completed(async_tasks), total=len(async_tasks), desc="Generando"):
|
|
522
|
+
try:
|
|
523
|
+
result = loop.run_until_complete(coro)
|
|
524
|
+
if result:
|
|
525
|
+
valid_variations.append(result)
|
|
526
|
+
except Exception as e:
|
|
527
|
+
logger.error(f"Excepción no manejada en async worker: {e}")
|
|
528
|
+
|
|
529
|
+
logger.info(f"Generación async completada. {len(valid_variations)} variaciones exitosas.")
|
|
530
|
+
return valid_variations
|
|
262
531
|
|
|
263
|
-
def generate_exam_files(
|
|
264
|
-
|
|
532
|
+
def generate_exam_files(
|
|
533
|
+
self,
|
|
534
|
+
variations: List[Dict],
|
|
535
|
+
args: argparse.Namespace,
|
|
536
|
+
output_dir: Union[Path, str],
|
|
537
|
+
exam_number: int
|
|
538
|
+
) -> bool:
|
|
539
|
+
"""Paso 5: Genera los archivos finales del examen."""
|
|
265
540
|
logger.info("Paso 5: Generando archivos de examen...")
|
|
266
541
|
exam_gen = ExamGenerator(self.base_path)
|
|
267
542
|
|
evolutia/exam_generator.py
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
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.
|
|
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, Union
|
|
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: Union[Path, str]):
|
|
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: Optional[List[str]] = None) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Genera el frontmatter YAML para un examen.
|
|
30
30
|
|
|
31
31
|
Args:
|
|
32
32
|
exam_number: Número del examen
|
|
@@ -268,17 +268,18 @@ class ExamGenerator:
|
|
|
268
268
|
f.write(self.generate_solution_file(
|
|
269
269
|
solution_content, i, exam_number, current_metadata
|
|
270
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
271
|
+
logger.info(f"[ExamGenerator] Solución creada: {solution_file}")
|
|
272
|
+
else:
|
|
273
|
+
logger.warning(f"[ExamGenerator] 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
|
+
logger.info(f"[ExamGenerator] Examen generado exitosamente en {output_dir}")
|
|
279
|
+
return True
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.error(f"[ExamGenerator] Error generando examen (output_dir={output_dir}, exam_number={exam_number}): {e}")
|
|
282
|
+
return False
|
|
282
283
|
|
|
283
284
|
def _update_downloads_in_frontmatter(self, exam_file: Path, exam_number: int,
|
|
284
285
|
num_exercises: int):
|
|
@@ -322,7 +323,7 @@ class ExamGenerator:
|
|
|
322
323
|
)
|
|
323
324
|
new_content = f"---\n{new_frontmatter_str}---\n\n" + content[frontmatter_match.end():]
|
|
324
325
|
|
|
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}")
|
|
326
|
+
exam_file.write_text(new_content, encoding='utf-8')
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.warning(f"[ExamGenerator] No se pudo actualizar downloads en {exam_file}: {e}")
|
|
328
329
|
|
evolutia/exceptions.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Excepciones personalizadas para EvolutIA.
|
|
3
|
+
Define una jerarquía de excepciones para manejo consistente de errores.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
class EvolutiaError(Exception):
|
|
7
|
+
"""Excepción base para todos los errores de EvolutIA."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigurationError(EvolutiaError):
|
|
12
|
+
"""Error en la configuración del sistema."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProviderError(EvolutiaError):
|
|
17
|
+
"""Error en el proveedor de LLM."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ValidationError(EvolutiaError):
|
|
22
|
+
"""Error de validación de datos."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MaterialExtractionError(EvolutiaError):
|
|
27
|
+
"""Error al extraer materiales didácticos."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExamGenerationError(EvolutiaError):
|
|
32
|
+
"""Error al generar examen."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RAGError(EvolutiaError):
|
|
37
|
+
"""Error en el sistema RAG."""
|
|
38
|
+
pass
|