evolutia 0.1.3__tar.gz → 0.1.5__tar.gz

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.
Files changed (54) hide show
  1. evolutia-0.1.5/MANIFEST.in +3 -0
  2. {evolutia-0.1.3 → evolutia-0.1.5}/PKG-INFO +1 -1
  3. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/evolutia_engine.py +249 -237
  4. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/llm_providers.py +14 -2
  5. evolutia-0.1.5/evolutia/schemas/config.schema.json +210 -0
  6. evolutia-0.1.5/evolutia/validation/__init__.py +2 -0
  7. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/variation_generator.py +86 -86
  8. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia.egg-info/PKG-INFO +1 -1
  9. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia.egg-info/SOURCES.txt +2 -0
  10. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia_cli.py +20 -19
  11. {evolutia-0.1.3 → evolutia-0.1.5}/setup.py +2 -1
  12. evolutia-0.1.3/evolutia/validation/__init__.py +0 -1
  13. {evolutia-0.1.3 → evolutia-0.1.5}/LICENSE +0 -0
  14. {evolutia-0.1.3 → evolutia-0.1.5}/README.md +0 -0
  15. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/__init__.py +0 -0
  16. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/async_llm_providers.py +0 -0
  17. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/cache/__init__.py +0 -0
  18. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/cache/exercise_cache.py +0 -0
  19. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/cache/llm_cache.py +0 -0
  20. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/complexity_validator.py +0 -0
  21. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/config_manager.py +0 -0
  22. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/exam_generator.py +0 -0
  23. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/exceptions.py +0 -0
  24. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/exercise_analyzer.py +0 -0
  25. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/imports.py +0 -0
  26. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/material_extractor.py +0 -0
  27. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/rag/__init__.py +0 -0
  28. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/rag/consistency_validator.py +0 -0
  29. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/rag/context_enricher.py +0 -0
  30. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/rag/enhanced_variation_generator.py +0 -0
  31. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/rag/rag_indexer.py +0 -0
  32. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/rag/rag_manager.py +0 -0
  33. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/rag/rag_retriever.py +0 -0
  34. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/retry_utils.py +0 -0
  35. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/utils/__init__.py +0 -0
  36. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/utils/json_parser.py +0 -0
  37. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/utils/markdown_parser.py +0 -0
  38. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/utils/math_extractor.py +0 -0
  39. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/validation/args_validator.py +0 -0
  40. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia/validation/config_validator.py +0 -0
  41. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia.egg-info/dependency_links.txt +0 -0
  42. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia.egg-info/entry_points.txt +0 -0
  43. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia.egg-info/requires.txt +0 -0
  44. {evolutia-0.1.3 → evolutia-0.1.5}/evolutia.egg-info/top_level.txt +0 -0
  45. {evolutia-0.1.3 → evolutia-0.1.5}/setup.cfg +0 -0
  46. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_args_validator.py +0 -0
  47. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_cache.py +0 -0
  48. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_complexity_validator.py +0 -0
  49. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_config_discovery.py +0 -0
  50. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_config_validator.py +0 -0
  51. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_exercise_analyzer.py +0 -0
  52. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_exercise_analyzer_cache.py +0 -0
  53. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_json_robustness.py +0 -0
  54. {evolutia-0.1.3 → evolutia-0.1.5}/tests/test_math_extractor.py +0 -0
@@ -0,0 +1,3 @@
1
+ recursive-include evolutia/schemas *.json
2
+ include README.md
3
+ include LICENSE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evolutia
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Sistema automatizado para generar preguntas de examen desafiantes basadas en materiales didácticos existentes
5
5
  Home-page: https://github.com/glacy/evolutIA
6
6
  Author: Gerardo Lacy-Mora
@@ -11,40 +11,40 @@ import time
11
11
  from pathlib import Path
12
12
  from typing import List, Dict, Optional, Tuple, Any, Union
13
13
  from tqdm import tqdm
14
-
15
- # Imports from internal modules
16
- from .material_extractor import MaterialExtractor
17
- from .exercise_analyzer import ExerciseAnalyzer
18
- from .variation_generator import VariationGenerator
19
- from .complexity_validator import ComplexityValidator
20
- from .exam_generator import ExamGenerator
21
- from .config_manager import ConfigManager
22
-
23
- # Conditional RAG imports
24
- try:
25
- from .rag.rag_manager import RAGManager
26
- from .rag.enhanced_variation_generator import EnhancedVariationGenerator
27
- from .rag.consistency_validator import ConsistencyValidator
28
- RAG_AVAILABLE = True
29
- except ImportError:
30
- RAG_AVAILABLE = False
31
-
32
- logger = logging.getLogger(__name__)
33
-
34
- class EvolutiaEngine:
35
- """
36
- Motor central que coordina el flujo de trabajo de EvolutIA.
37
- """
38
-
14
+
15
+ # Imports from internal modules
16
+ from .material_extractor import MaterialExtractor
17
+ from .exercise_analyzer import ExerciseAnalyzer
18
+ from .variation_generator import VariationGenerator
19
+ from .complexity_validator import ComplexityValidator
20
+ from .exam_generator import ExamGenerator
21
+ from .config_manager import ConfigManager
22
+
23
+ # Conditional RAG imports
24
+ try:
25
+ from .rag.rag_manager import RAGManager
26
+ from .rag.enhanced_variation_generator import EnhancedVariationGenerator
27
+ from .rag.consistency_validator import ConsistencyValidator
28
+ RAG_AVAILABLE = True
29
+ except ImportError:
30
+ RAG_AVAILABLE = False
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ class EvolutiaEngine:
35
+ """
36
+ Motor central que coordina el flujo de trabajo de EvolutIA.
37
+ """
38
+
39
39
  def __init__(self, base_path: Union[Path, str], config_path: Optional[Union[Path, str]] = None):
40
40
  self.base_path = Path(base_path)
41
41
  self.config_path = Path(config_path) if config_path else None
42
42
  self.rag_manager = None
43
-
44
- # Load configuration manager
45
- self.config_manager = ConfigManager(base_path, config_path)
46
- self.full_config = self.config_manager.load_current_config()
47
-
43
+
44
+ # Load configuration manager
45
+ self.config_manager = ConfigManager(base_path, config_path)
46
+ self.full_config = self.config_manager.load_current_config()
47
+
48
48
  def initialize_rag(self, force_reindex: bool = False) -> bool:
49
49
  """
50
50
  Inicializa el subsistema RAG si está disponible.
@@ -65,70 +65,70 @@ class EvolutiaEngine:
65
65
  except Exception as e:
66
66
  logger.error(f"[EvolutiaEngine] Error inicializando RAG (force_reindex={force_reindex}): {e}")
67
67
  return False
68
-
69
- def get_api_config(self, provider: str) -> Dict[str, Any]:
70
- """Obtiene la configuración específica para una API."""
71
- return self.full_config.get('api', {}).get(provider, {})
72
-
73
- def extract_materials_and_exercises(self, topics: List[str], label_filter: Optional[List[str]] = None) -> Tuple[List[Dict], List[Dict]]:
74
- """
75
- Paso 1 & 2: Extrae materiales y lista todos los ejercicios disponibles.
76
- """
77
- logger.info("Paso 1: Extrayendo materiales didácticos...")
78
- extractor = MaterialExtractor(self.base_path)
79
- materials = []
80
-
81
- # 1. Extract by topic
82
- if topics:
83
- for topic in topics:
84
- topic_materials = extractor.extract_by_topic(topic)
85
- if topic_materials:
86
- materials.extend(topic_materials)
87
- else:
88
- logger.warning(f"No se encontraron materiales para el tema: {topic}")
89
-
90
- # 2. Fallback: Search all if no materials found yet or topics were empty (e.g., list mode)
91
- if not materials:
92
- logger.info("Buscando en todos los directorios...")
93
- for topic_dir in self.base_path.iterdir():
94
- if topic_dir.is_dir() and topic_dir.name not in ['_build', 'evolutia', 'proyecto', '.git']:
95
- materials.extend(extractor.extract_from_directory(topic_dir))
96
-
97
- if not materials:
98
- return [], []
99
-
100
- logger.info(f"Encontrados {len(materials)} archivos con materiales")
101
-
102
- # Get exercises
103
- logger.info("Paso 2: Obteniendo ejercicios...")
104
- all_exercises = extractor.get_all_exercises(materials)
105
-
106
- # Filter by label if requested
107
- if label_filter:
108
- logger.info(f"Filtrando por labels: {label_filter}")
109
- filtered = [ex for ex in all_exercises if ex.get('label') in label_filter]
110
- if not filtered:
111
- available = [ex.get('label') for ex in all_exercises if ex.get('label')]
112
- logger.warning(f"No se encontraron ejercicios con los labels solicitados. Disponibles: {available[:10]}...")
113
- all_exercises = filtered
114
-
115
- logger.info(f"Encontrados {len(all_exercises)} ejercicios")
116
- return materials, all_exercises
117
-
118
- def analyze_exercises(self, exercises: List[Dict]) -> List[Tuple[Dict, Dict]]:
119
- """Paso 3: Analiza la complejidad de los ejercicios."""
120
- logger.info("Paso 3: Analizando complejidad de ejercicios...")
121
- analyzer = ExerciseAnalyzer()
122
- exercises_with_analysis = []
123
-
124
- for exercise in exercises:
125
- analysis = analyzer.analyze(exercise)
126
- exercises_with_analysis.append((exercise, analysis))
127
-
128
- # Sort by total complexity descending
129
- exercises_with_analysis.sort(key=lambda x: x[1]['total_complexity'], reverse=True)
130
- return exercises_with_analysis
131
-
68
+
69
+ def get_api_config(self, provider: str) -> Dict[str, Any]:
70
+ """Obtiene la configuración específica para una API."""
71
+ return self.full_config.get('api', {}).get(provider, {})
72
+
73
+ def extract_materials_and_exercises(self, topics: List[str], label_filter: Optional[List[str]] = None) -> Tuple[List[Dict], List[Dict]]:
74
+ """
75
+ Paso 1 & 2: Extrae materiales y lista todos los ejercicios disponibles.
76
+ """
77
+ logger.info("Paso 1: Extrayendo materiales didácticos...")
78
+ extractor = MaterialExtractor(self.base_path)
79
+ materials = []
80
+
81
+ # 1. Extract by topic
82
+ if topics:
83
+ for topic in topics:
84
+ topic_materials = extractor.extract_by_topic(topic)
85
+ if topic_materials:
86
+ materials.extend(topic_materials)
87
+ else:
88
+ logger.warning(f"No se encontraron materiales para el tema: {topic}")
89
+
90
+ # 2. Fallback: Search all if no materials found yet or topics were empty (e.g., list mode)
91
+ if not materials:
92
+ logger.info("Buscando en todos los directorios...")
93
+ for topic_dir in self.base_path.iterdir():
94
+ if topic_dir.is_dir() and topic_dir.name not in ['_build', 'evolutia', 'proyecto', '.git']:
95
+ materials.extend(extractor.extract_from_directory(topic_dir))
96
+
97
+ if not materials:
98
+ return [], []
99
+
100
+ logger.info(f"Encontrados {len(materials)} archivos con materiales")
101
+
102
+ # Get exercises
103
+ logger.info("Paso 2: Obteniendo ejercicios...")
104
+ all_exercises = extractor.get_all_exercises(materials)
105
+
106
+ # Filter by label if requested
107
+ if label_filter:
108
+ logger.info(f"Filtrando por labels: {label_filter}")
109
+ filtered = [ex for ex in all_exercises if ex.get('label') in label_filter]
110
+ if not filtered:
111
+ available = [ex.get('label') for ex in all_exercises if ex.get('label')]
112
+ logger.warning(f"No se encontraron ejercicios con los labels solicitados. Disponibles: {available[:10]}...")
113
+ all_exercises = filtered
114
+
115
+ logger.info(f"Encontrados {len(all_exercises)} ejercicios")
116
+ return materials, all_exercises
117
+
118
+ def analyze_exercises(self, exercises: List[Dict]) -> List[Tuple[Dict, Dict]]:
119
+ """Paso 3: Analiza la complejidad de los ejercicios."""
120
+ logger.info("Paso 3: Analizando complejidad de ejercicios...")
121
+ analyzer = ExerciseAnalyzer()
122
+ exercises_with_analysis = []
123
+
124
+ for exercise in exercises:
125
+ analysis = analyzer.analyze(exercise)
126
+ exercises_with_analysis.append((exercise, analysis))
127
+
128
+ # Sort by total complexity descending
129
+ exercises_with_analysis.sort(key=lambda x: x[1]['total_complexity'], reverse=True)
130
+ return exercises_with_analysis
131
+
132
132
  def _generate_single_variation(
133
133
  self,
134
134
  generator: Union['VariationGenerator', 'EnhancedVariationGenerator'],
@@ -138,43 +138,46 @@ class EvolutiaEngine:
138
138
  args: argparse.Namespace
139
139
  ) -> Optional[Dict]:
140
140
  """Helper para generar una única variación (thread-safe logic)."""
141
- attempt_count = 0
142
- while attempt_count < 3:
143
- try:
144
- # Generate
145
- if args.type == 'multiple_choice':
146
- variation = generator.generate_variation(
147
- exercise_base,
148
- analysis,
149
- exercise_type=args.type
150
- )
151
- elif not args.no_generar_soluciones:
152
- variation = generator.generate_variation_with_solution(
153
- exercise_base,
154
- analysis
155
- )
156
- else:
157
- variation = generator.generate_variation(
158
- exercise_base,
159
- analysis,
160
- exercise_type=args.type
161
- )
162
-
163
- if not variation:
164
- attempt_count += 1
165
- continue
166
-
167
- # Validate
168
- if args.use_rag:
169
- validation = validator.validate(exercise_base, analysis, variation)
170
- is_valid = validation['is_valid']
171
- else:
172
- validation = validator.validate(exercise_base, analysis, variation)
173
- is_valid = validation['is_valid']
174
-
175
- if is_valid:
176
- return variation
177
-
141
+ attempt_count = 0
142
+ while attempt_count < 3:
143
+ try:
144
+ # Generate
145
+ if args.type == 'multiple_choice':
146
+ variation = generator.generate_variation(
147
+ exercise_base,
148
+ analysis,
149
+ exercise_type=args.type,
150
+ max_tokens=args.max_tokens
151
+ )
152
+ elif not args.no_generar_soluciones:
153
+ variation = generator.generate_variation_with_solution(
154
+ exercise_base,
155
+ analysis,
156
+ max_tokens=args.max_tokens
157
+ )
158
+ else:
159
+ variation = generator.generate_variation(
160
+ exercise_base,
161
+ analysis,
162
+ exercise_type=args.type,
163
+ max_tokens=args.max_tokens
164
+ )
165
+
166
+ if not variation:
167
+ attempt_count += 1
168
+ continue
169
+
170
+ # Validate
171
+ if args.use_rag:
172
+ validation = validator.validate(exercise_base, analysis, variation)
173
+ is_valid = validation['is_valid']
174
+ else:
175
+ validation = validator.validate(exercise_base, analysis, variation)
176
+ is_valid = validation['is_valid']
177
+
178
+ if is_valid:
179
+ return variation
180
+
178
181
  except Exception as e:
179
182
  logger.error(f"[EvolutiaEngine] Error en hilo de generación (intento {attempt_count + 1}/3): {e}")
180
183
 
@@ -188,7 +191,8 @@ class EvolutiaEngine:
188
191
  topic: str,
189
192
  tags: List[str],
190
193
  complexity: str,
191
- ex_type: str
194
+ ex_type: str,
195
+ args: argparse.Namespace
192
196
  ) -> Optional[Dict]:
193
197
  """
194
198
  Helper para modo creación.
@@ -202,12 +206,13 @@ class EvolutiaEngine:
202
206
  topic,
203
207
  tags,
204
208
  difficulty=complexity,
205
- exercise_type=ex_type
209
+ exercise_type=ex_type,
210
+ max_tokens=args.max_tokens
206
211
  )
207
212
  except Exception as e:
208
213
  logger.error(f"[EvolutiaEngine] Error en creación de ejercicio nuevo (topic={topic}): {e}")
209
214
  return None
210
-
215
+
211
216
  def generate_variations_parallel(
212
217
  self,
213
218
  selected_exercises: List[Tuple[Dict, Dict]],
@@ -217,83 +222,83 @@ class EvolutiaEngine:
217
222
  """
218
223
  Paso 4: Genera variaciones en paralelo.
219
224
  """
220
- logger.info(f"Paso 4: Generando variaciones en paralelo (Workers: {max_workers})...")
221
-
222
- # Setup Generator
223
- api_config = self.get_api_config(args.api)
224
-
225
- if (args.use_rag and self.rag_manager) or args.mode == 'creation':
226
- retriever = self.rag_manager.get_retriever() if (args.use_rag and self.rag_manager) else None
227
- generator = EnhancedVariationGenerator(api_provider=args.api, retriever=retriever)
228
- validator = ConsistencyValidator(retriever=retriever) if retriever else ComplexityValidator()
229
- else:
230
- generator = VariationGenerator(api_provider=args.api)
231
- validator = ComplexityValidator()
232
-
233
- # Configure model
234
- if args.api == 'local':
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:
244
- generator.model_name = api_config['model']
245
-
246
- # Determine tasks based on mode
247
- tasks = []
248
-
249
- if args.mode == 'creation':
250
- # Creation Mode Logic
251
- for i in range(args.num_ejercicios):
252
- current_topic = args.tema[i % len(args.tema)]
253
- current_tags = [args.tags[i % len(args.tags)]] if args.tags else [current_topic]
254
-
255
- tasks.append({
256
- 'func': self._generate_creation_mode,
257
- 'args': (generator, current_topic, current_tags, args.complejidad, args.type)
258
- })
259
- else:
260
- # Variation Mode Logic
261
-
262
- # If explicit lables, use exactly those
263
- if args.label:
264
- target_exercises = list(selected_exercises)
265
- else:
266
- # Random selection to fill num_ejercicios
267
- target_exercises = []
268
- candidates = selected_exercises[:max(5, len(selected_exercises)//2)]
269
- for _ in range(args.num_ejercicios):
270
- if candidates:
271
- target_exercises.append(random.choice(candidates))
272
-
273
- for ex_base, analysis in target_exercises:
274
- tasks.append({
275
- 'func': self._generate_single_variation,
276
- 'args': (generator, validator, ex_base, analysis, args)
277
- })
278
-
279
- # Execute Parallel
280
- valid_variations = []
281
- with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
282
- future_to_task = {}
283
- for t in tasks:
284
- future = executor.submit(t['func'], *t['args'])
285
- future_to_task[future] = t
286
- # Stagger requests to avoid hitting rate limits instantly
287
- time.sleep(1.0)
288
-
289
- for future in tqdm(concurrent.futures.as_completed(future_to_task), total=len(tasks), desc="Generando"):
290
- try:
291
- result = future.result()
292
- if result:
293
- valid_variations.append(result)
294
- except Exception as e:
295
- logger.error(f"Excepción no manejada en worker: {e}")
296
-
225
+ logger.info(f"Paso 4: Generando variaciones en paralelo (Workers: {max_workers})...")
226
+
227
+ # Setup Generator
228
+ api_config = self.get_api_config(args.api)
229
+
230
+ if (args.use_rag and self.rag_manager) or args.mode == 'creation':
231
+ retriever = self.rag_manager.get_retriever() if (args.use_rag and self.rag_manager) else None
232
+ generator = EnhancedVariationGenerator(api_provider=args.api, retriever=retriever)
233
+ validator = ConsistencyValidator(retriever=retriever) if retriever else ComplexityValidator()
234
+ else:
235
+ generator = VariationGenerator(api_provider=args.api)
236
+ validator = ComplexityValidator()
237
+
238
+ # Configure model
239
+ if args.api == 'local':
240
+ generator.base_url = args.base_url or api_config.get('base_url', "http://localhost:11434/v1")
241
+ generator.local_model = args.model or api_config.get('model', "llama3")
242
+ elif args.api == 'generic':
243
+ generator.base_url = args.base_url or api_config.get('base_url')
244
+ generator.model_name = args.model or api_config.get('model')
245
+ elif args.api in ['openai', 'anthropic', 'deepseek', 'gemini']:
246
+ if args.model:
247
+ generator.model_name = args.model
248
+ elif 'model' in api_config:
249
+ generator.model_name = api_config['model']
250
+
251
+ # Determine tasks based on mode
252
+ tasks = []
253
+
254
+ if args.mode == 'creation':
255
+ # Creation Mode Logic
256
+ for i in range(args.num_ejercicios):
257
+ current_topic = args.tema[i % len(args.tema)]
258
+ current_tags = [args.tags[i % len(args.tags)]] if args.tags else [current_topic]
259
+
260
+ tasks.append({
261
+ 'func': self._generate_creation_mode,
262
+ 'args': (generator, current_topic, current_tags, args.complejidad, args.type, args)
263
+ })
264
+ else:
265
+ # Variation Mode Logic
266
+
267
+ # If explicit lables, use exactly those
268
+ if args.label:
269
+ target_exercises = list(selected_exercises)
270
+ else:
271
+ # Random selection to fill num_ejercicios
272
+ target_exercises = []
273
+ candidates = selected_exercises[:max(5, len(selected_exercises)//2)]
274
+ for _ in range(args.num_ejercicios):
275
+ if candidates:
276
+ target_exercises.append(random.choice(candidates))
277
+
278
+ for ex_base, analysis in target_exercises:
279
+ tasks.append({
280
+ 'func': self._generate_single_variation,
281
+ 'args': (generator, validator, ex_base, analysis, args)
282
+ })
283
+
284
+ # Execute Parallel
285
+ valid_variations = []
286
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
287
+ future_to_task = {}
288
+ for t in tasks:
289
+ future = executor.submit(t['func'], *t['args'])
290
+ future_to_task[future] = t
291
+ # Stagger requests to avoid hitting rate limits instantly
292
+ time.sleep(1.0)
293
+
294
+ for future in tqdm(concurrent.futures.as_completed(future_to_task), total=len(tasks), desc="Generando"):
295
+ try:
296
+ result = future.result()
297
+ if result:
298
+ valid_variations.append(result)
299
+ except Exception as e:
300
+ logger.error(f"Excepción no manejada en worker: {e}")
301
+
297
302
  logger.info(f"Generación completada. {len(valid_variations)} variaciones exitosas.")
298
303
  return valid_variations
299
304
 
@@ -332,14 +337,16 @@ class EvolutiaEngine:
332
337
  generator.generate_variation,
333
338
  exercise_base,
334
339
  analysis,
335
- args.type
340
+ exercise_type=args.type,
341
+ max_tokens=args.max_tokens
336
342
  )
337
343
  elif not args.no_generar_soluciones:
338
344
  variation = await loop.run_in_executor(
339
345
  None,
340
346
  generator.generate_variation_with_solution,
341
347
  exercise_base,
342
- analysis
348
+ analysis,
349
+ max_tokens=args.max_tokens
343
350
  )
344
351
  else:
345
352
  variation = await loop.run_in_executor(
@@ -347,7 +354,8 @@ class EvolutiaEngine:
347
354
  generator.generate_variation,
348
355
  exercise_base,
349
356
  analysis,
350
- args.type
357
+ exercise_type=args.type,
358
+ max_tokens=args.max_tokens
351
359
  )
352
360
 
353
361
  if not variation:
@@ -381,6 +389,7 @@ class EvolutiaEngine:
381
389
  tags: List[str],
382
390
  complexity: str,
383
391
  ex_type: str,
392
+ args: argparse.Namespace,
384
393
  semaphore: asyncio.Semaphore
385
394
  ) -> Optional[Dict]:
386
395
  """
@@ -399,7 +408,8 @@ class EvolutiaEngine:
399
408
  topic,
400
409
  tags,
401
410
  complexity,
402
- ex_type
411
+ ex_type,
412
+ args.max_tokens
403
413
  )
404
414
  except Exception as e:
405
415
  logger.error(f"[EvolutiaEngine] Error en async creación (topic={topic}): {e}")
@@ -463,7 +473,8 @@ class EvolutiaEngine:
463
473
  current_topic,
464
474
  current_tags,
465
475
  args.complejidad,
466
- args.type
476
+ args.type,
477
+ args.max_tokens
467
478
  ))
468
479
  else:
469
480
  if args.label:
@@ -498,6 +509,7 @@ class EvolutiaEngine:
498
509
  task_info[3], # tags
499
510
  task_info[4], # complexity
500
511
  task_info[5], # ex_type
512
+ task_info[6], # max_tokens
501
513
  semaphore
502
514
  ))
503
515
  else:
@@ -528,7 +540,7 @@ class EvolutiaEngine:
528
540
 
529
541
  logger.info(f"Generación async completada. {len(valid_variations)} variaciones exitosas.")
530
542
  return valid_variations
531
-
543
+
532
544
  def generate_exam_files(
533
545
  self,
534
546
  variations: List[Dict],
@@ -537,23 +549,23 @@ class EvolutiaEngine:
537
549
  exam_number: int
538
550
  ) -> bool:
539
551
  """Paso 5: Genera los archivos finales del examen."""
540
- logger.info("Paso 5: Generando archivos de examen...")
541
- exam_gen = ExamGenerator(self.base_path)
542
-
543
- keywords = args.keywords or []
544
- metadata = {
545
- 'model': args.api, # Simplified, internal details hidden
546
- 'provider': args.api,
547
- 'rag_enabled': args.use_rag,
548
- 'mode': args.mode,
549
- 'target_difficulty': args.complejidad
550
- }
551
-
552
- return exam_gen.generate_exam(
553
- variations,
554
- exam_number,
555
- output_dir,
556
- args.subject,
557
- keywords,
558
- metadata=metadata
559
- )
552
+ logger.info("Paso 5: Generando archivos de examen...")
553
+ exam_gen = ExamGenerator(self.base_path)
554
+
555
+ keywords = args.keywords or []
556
+ metadata = {
557
+ 'model': args.api, # Simplified, internal details hidden
558
+ 'provider': args.api,
559
+ 'rag_enabled': args.use_rag,
560
+ 'mode': args.mode,
561
+ 'target_difficulty': args.complejidad
562
+ }
563
+
564
+ return exam_gen.generate_exam(
565
+ variations,
566
+ exam_number,
567
+ output_dir,
568
+ args.subject,
569
+ keywords,
570
+ metadata=metadata
571
+ )
@@ -142,8 +142,20 @@ class OpenAICompatibleProvider(LLMProvider):
142
142
  temperature=kwargs.get("temperature", self.DEFAULT_TEMPERATURE),
143
143
  max_tokens=kwargs.get("max_tokens", self.DEFAULT_MAX_TOKENS)
144
144
  )
145
- content = response.choices[0].message.content.strip()
146
- logger.info(f"[{provider_name}] Contenido generado exitosamente (modelo={model}, longitud={len(content)})")
145
+ message = response.choices[0].message
146
+ finish_reason = response.choices[0].finish_reason
147
+
148
+ if hasattr(message, 'content') and message.content is not None:
149
+ content = message.content.strip()
150
+ else:
151
+ content = ""
152
+ logger.warning(f"[{provider_name}] Message content is None")
153
+
154
+ if not content:
155
+ logger.warning(f"[{provider_name}] Contenido vacío recibido. Finish reason: {finish_reason}")
156
+ logger.debug(f"[{provider_name}] Raw response: {response}")
157
+
158
+ logger.info(f"[{provider_name}] Contenido generado exitosamente (modelo={model}, longitud={len(content)}, reason={finish_reason})")
147
159
 
148
160
  # Guardar en caché
149
161
  if self.cache: