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.
@@ -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 logging
6
- import random
7
- import concurrent.futures
8
- import time
9
- from pathlib import Path
10
- from typing import List, Dict, Optional, Tuple, Any
11
- from tqdm import tqdm
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
- """Inicializa el subsistema RAG si está disponible."""
48
- if not RAG_AVAILABLE:
49
- logger.error("RAG solicitado pero no disponible. Instala dependencias.")
50
- return False
51
-
52
- try:
53
- self.rag_manager = RAGManager(config_path=self.config_path, base_path=self.base_path)
54
- self.rag_manager.initialize(force_reindex=force_reindex)
55
- return True
56
- except Exception as e:
57
- logger.error(f"Error inicializando RAG: {e}")
58
- return False
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(self, generator, validator, exercise_base, analysis, args) -> Optional[Dict]:
124
- """Helper para generar una única variación (thread-safe logic)."""
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
- return None
167
-
168
- def _generate_creation_mode(self, generator, topic, tags, complexity, ex_type) -> Optional[Dict]:
169
- """Helper para modo creación."""
170
- try:
171
- return generator.generate_new_exercise_from_topic(
172
- topic,
173
- tags,
174
- difficulty=complexity,
175
- exercise_type=ex_type
176
- )
177
- except Exception as e:
178
- logger.error(f"Error en creación de ejercicio nuevo: {e}")
179
- return None
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(self,
182
- selected_exercises: List[Tuple[Dict, Dict]],
183
- args,
184
- max_workers: int = 5) -> List[Dict]:
185
- """
186
- Paso 4: Genera variaciones en paralelo.
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 in ['openai', 'anthropic']:
206
- if 'model' in api_config:
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(self, variations: List[Dict], args, output_dir: Path, exam_number: int) -> bool:
264
- """Paso 5: Genera los archivos finales del examen."""
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
 
@@ -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
- return True
279
- except Exception as e:
280
- logger.error(f"Error generando examen: {e}")
281
- return False
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