evolutia 0.1.3__py3-none-any.whl → 0.1.5__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.
@@ -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
+ )
evolutia/llm_providers.py CHANGED
@@ -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:
@@ -0,0 +1,210 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Evolutia Configuration",
4
+ "description": "Configuration schema for Evolutia AI tool",
5
+ "type": "object",
6
+ "properties": {
7
+ "api": {
8
+ "type": "object",
9
+ "properties": {
10
+ "default_provider": {
11
+ "type": "string"
12
+ },
13
+ "openai": {
14
+ "$ref": "#/definitions/modelConfig"
15
+ },
16
+ "anthropic": {
17
+ "$ref": "#/definitions/modelConfig"
18
+ },
19
+ "gemini": {
20
+ "$ref": "#/definitions/modelConfig"
21
+ },
22
+ "local": {
23
+ "allOf": [
24
+ {
25
+ "$ref": "#/definitions/modelConfig"
26
+ },
27
+ {
28
+ "properties": {
29
+ "base_url": {
30
+ "type": "string"
31
+ }
32
+ }
33
+ }
34
+ ]
35
+ }
36
+ },
37
+ "required": [
38
+ "default_provider"
39
+ ]
40
+ },
41
+ "paths": {
42
+ "type": "object",
43
+ "properties": {
44
+ "base_path": {
45
+ "type": "string"
46
+ },
47
+ "materials_directories": {
48
+ "type": "array",
49
+ "items": {
50
+ "type": "string"
51
+ }
52
+ }
53
+ },
54
+ "required": [
55
+ "materials_directories"
56
+ ]
57
+ },
58
+ "complexity": {
59
+ "type": "object",
60
+ "properties": {
61
+ "min_complexity_increase": {
62
+ "type": "number"
63
+ },
64
+ "min_improvements": {
65
+ "type": "integer"
66
+ },
67
+ "weights": {
68
+ "type": "object",
69
+ "patternProperties": {
70
+ "^.*$": {
71
+ "type": "number"
72
+ }
73
+ }
74
+ }
75
+ }
76
+ },
77
+ "exam": {
78
+ "type": "object",
79
+ "properties": {
80
+ "default": {
81
+ "type": "object",
82
+ "properties": {
83
+ "subject": {
84
+ "type": "string"
85
+ },
86
+ "points_per_exercise": {
87
+ "type": "integer"
88
+ },
89
+ "duration_hours": {
90
+ "type": "number"
91
+ }
92
+ }
93
+ },
94
+ "keywords": {
95
+ "type": "object",
96
+ "patternProperties": {
97
+ "^.*$": {
98
+ "type": "array",
99
+ "items": {
100
+ "type": "string"
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ },
107
+ "logging": {
108
+ "type": "object",
109
+ "properties": {
110
+ "level": {
111
+ "type": "string"
112
+ },
113
+ "format": {
114
+ "type": "string"
115
+ }
116
+ }
117
+ },
118
+ "rag": {
119
+ "type": "object",
120
+ "properties": {
121
+ "vector_store": {
122
+ "type": "object",
123
+ "properties": {
124
+ "type": {
125
+ "type": "string"
126
+ },
127
+ "persist_directory": {
128
+ "type": "string"
129
+ },
130
+ "collection_name": {
131
+ "type": "string"
132
+ }
133
+ },
134
+ "required": [
135
+ "type"
136
+ ]
137
+ },
138
+ "embeddings": {
139
+ "type": "object",
140
+ "properties": {
141
+ "provider": {
142
+ "type": "string"
143
+ },
144
+ "model": {
145
+ "type": "string"
146
+ },
147
+ "batch_size": {
148
+ "type": "integer"
149
+ }
150
+ },
151
+ "required": [
152
+ "provider",
153
+ "model"
154
+ ]
155
+ },
156
+ "retrieval": {
157
+ "type": "object",
158
+ "properties": {
159
+ "top_k": {
160
+ "type": "integer"
161
+ },
162
+ "similarity_threshold": {
163
+ "type": "number"
164
+ },
165
+ "use_metadata_filters": {
166
+ "type": "boolean"
167
+ }
168
+ }
169
+ },
170
+ "chunking": {
171
+ "type": "object",
172
+ "properties": {
173
+ "chunk_size": {
174
+ "type": "integer"
175
+ },
176
+ "chunk_overlap": {
177
+ "type": "integer"
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ },
184
+ "required": [
185
+ "api",
186
+ "paths"
187
+ ],
188
+ "definitions": {
189
+ "modelConfig": {
190
+ "type": "object",
191
+ "properties": {
192
+ "model": {
193
+ "type": "string"
194
+ },
195
+ "temperature": {
196
+ "type": "number"
197
+ },
198
+ "max_tokens": {
199
+ "type": "integer"
200
+ },
201
+ "api_key": {
202
+ "type": "string"
203
+ }
204
+ },
205
+ "required": [
206
+ "model"
207
+ ]
208
+ }
209
+ }
210
+ }
@@ -1 +1,2 @@
1
- # Tests package for EvolutIA validation
1
+ from .config_validator import ConfigValidator, ConfigValidationError
2
+ from .args_validator import ArgsValidator
@@ -1,22 +1,22 @@
1
- """
2
- Generador de variaciones de ejercicios con mayor complejidad.
3
- Utiliza APIs de IA para generar variaciones inteligentes.
4
- """
5
- import os
6
- import logging
7
- from typing import Dict, Optional, List, Union, Any
8
- from dotenv import load_dotenv
9
- from pathlib import Path
10
-
11
- # Imports for new Provider system
12
- from .llm_providers import get_provider, LLMProvider
13
- from .utils.json_parser import extract_and_parse_json
14
-
15
- # Cargar variables de entorno explícitamente desde el directorio del script
16
- env_path = Path(__file__).parent / '.env'
17
- load_dotenv(dotenv_path=env_path)
18
-
19
- logger = logging.getLogger(__name__)
1
+ """
2
+ Generador de variaciones de ejercicios con mayor complejidad.
3
+ Utiliza APIs de IA para generar variaciones inteligentes.
4
+ """
5
+ import os
6
+ import logging
7
+ from typing import Dict, Optional, List, Union, Any
8
+ from dotenv import load_dotenv
9
+ from pathlib import Path
10
+
11
+ # Imports for new Provider system
12
+ from .llm_providers import get_provider, LLMProvider
13
+ from .utils.json_parser import extract_and_parse_json
14
+
15
+ # Cargar variables de entorno explícitamente desde el directorio del script
16
+ env_path = Path(__file__).parent / '.env'
17
+ load_dotenv(dotenv_path=env_path)
18
+
19
+ logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
22
 
@@ -37,36 +37,36 @@ class VariationGenerator:
37
37
 
38
38
  self._provider_instance = None
39
39
 
40
- def _get_provider(self):
41
- """
42
- Lazy loader para el proveedor, permitiendo configuración tardía de props.
43
-
44
- Returns:
45
- Instancia de LLMProvider si la inicialización fue exitosa
46
- None si hubo un error de configuración
47
- """
48
- if self._provider_instance:
49
- return self._provider_instance
50
-
51
- kwargs = {}
52
- if self.model_name:
53
- kwargs['model_name'] = self.model_name
54
- elif self.local_model and self.api_provider == 'local':
55
- kwargs['model_name'] = self.local_model
56
-
57
- if self.base_url:
58
- kwargs['base_url'] = self.base_url
59
-
60
- try:
61
- self._provider_instance = get_provider(self.api_provider, **kwargs)
62
- logger.info(f"[VariationGenerator] Proveedor inicializado: {self.api_provider}")
63
- return self._provider_instance
64
- except ValueError as e:
65
- logger.error(f"[VariationGenerator] Error inicializando proveedor '{self.api_provider}': {e}")
66
- return None
67
- except Exception as e:
68
- logger.error(f"[VariationGenerator] Error inesperado inicializando proveedor '{self.api_provider}': {e}")
69
- return None
40
+ def _get_provider(self):
41
+ """
42
+ Lazy loader para el proveedor, permitiendo configuración tardía de props.
43
+
44
+ Returns:
45
+ Instancia de LLMProvider si la inicialización fue exitosa
46
+ None si hubo un error de configuración
47
+ """
48
+ if self._provider_instance:
49
+ return self._provider_instance
50
+
51
+ kwargs = {}
52
+ if self.model_name:
53
+ kwargs['model_name'] = self.model_name
54
+ elif self.local_model and self.api_provider == 'local':
55
+ kwargs['model_name'] = self.local_model
56
+
57
+ if self.base_url:
58
+ kwargs['base_url'] = self.base_url
59
+
60
+ try:
61
+ self._provider_instance = get_provider(self.api_provider, **kwargs)
62
+ logger.info(f"[VariationGenerator] Proveedor inicializado: {self.api_provider}")
63
+ return self._provider_instance
64
+ except ValueError as e:
65
+ logger.error(f"[VariationGenerator] Error inicializando proveedor '{self.api_provider}': {e}")
66
+ return None
67
+ except Exception as e:
68
+ logger.error(f"[VariationGenerator] Error inesperado inicializando proveedor '{self.api_provider}': {e}")
69
+ return None
70
70
 
71
71
  def _create_prompt(self, exercise: Dict, analysis: Dict) -> str:
72
72
  """Crea el prompt para generar una variación."""
@@ -105,8 +105,8 @@ REGLAS DE FORMATO:
105
105
  Genera solo el contenido solicitado."""
106
106
  return prompt
107
107
 
108
- def _create_quiz_prompt(self, context_info: Dict[str, Any]) -> str:
109
- """Crea prompt para ejercicios de selección única."""
108
+ def _create_quiz_prompt(self, context_info: Dict[str, Any]) -> str:
109
+ """Crea prompt para ejercicios de selección única."""
110
110
  content = context_info.get('content', '')
111
111
 
112
112
  prompt = f"""Actúa como un profesor experto. Genera una pregunta de examen de tipo SELECCIÓN ÚNICA (Quiz) basada en el siguiente material:
@@ -133,33 +133,33 @@ REQUISITOS:
133
133
  """
134
134
  return prompt
135
135
 
136
- def generate_variation(self, exercise: Dict[str, Any], analysis: Dict[str, Any], exercise_type: str = "development") -> Optional[Dict]:
137
- """
138
- Genera una variación de un ejercicio existente.
139
- """
140
- # 1. Crear prompt según tipo
141
- if exercise_type == 'multiple_choice':
142
- context_info = {
143
- 'content': f"Ejercicio Original:\n{exercise.get('content')}"
144
- }
145
- prompt = self._create_quiz_prompt(context_info)
146
- else:
147
- prompt = self._create_prompt(exercise, analysis)
148
-
149
- # 2. Get Provider
150
- provider = self._get_provider()
151
- if not provider:
152
- logger.warning("[VariationGenerator] Proveedor no inicializado, no se puede generar variación")
153
- return None
154
-
155
- # 3. Generar
156
- content = provider.generate_content(prompt, system_prompt="Eres un experto en diseño de exámenes de ingeniería.")
157
-
158
- if not content:
159
- logger.warning("[VariationGenerator] Proveedor retornó contenido vacío")
160
- return None
161
-
162
- # 4. Parsear respuesta
136
+ def generate_variation(self, exercise: Dict[str, Any], analysis: Dict[str, Any], exercise_type: str = "development", max_tokens: int = 2000) -> Optional[Dict]:
137
+ """
138
+ Genera una variación de un ejercicio existente.
139
+ """
140
+ # 1. Crear prompt según tipo
141
+ if exercise_type == 'multiple_choice':
142
+ context_info = {
143
+ 'content': f"Ejercicio Original:\n{exercise.get('content')}"
144
+ }
145
+ prompt = self._create_quiz_prompt(context_info)
146
+ else:
147
+ prompt = self._create_prompt(exercise, analysis)
148
+
149
+ # 2. Get Provider
150
+ provider = self._get_provider()
151
+ if not provider:
152
+ logger.warning("[VariationGenerator] Proveedor no inicializado, no se puede generar variación")
153
+ return None
154
+
155
+ # 3. Generar
156
+ content = provider.generate_content(prompt, system_prompt="Eres un experto en diseño de exámenes de ingeniería.", max_tokens=max_tokens)
157
+
158
+ if not content:
159
+ logger.warning("[VariationGenerator] Proveedor retornó contenido vacío")
160
+ return None
161
+
162
+ # 4. Parsear respuesta
163
163
  variation_content = ""
164
164
  variation_solution = ""
165
165
 
@@ -212,10 +212,10 @@ INSTRUCCIONES:
212
212
  """
213
213
  return prompt
214
214
 
215
- def generate_new_exercise_from_topic(self, topic: str, tags: Optional[List[str]] = None, difficulty: str = "alta", exercise_type: str = "development") -> Optional[Dict]:
216
- """
217
- Genera un ejercicio nuevo desde cero.
218
- """
215
+ def generate_new_exercise_from_topic(self, topic: str, tags: Optional[List[str]] = None, difficulty: str = "alta", exercise_type: str = "development", max_tokens: int = 2000) -> Optional[Dict]:
216
+ """
217
+ Genera un ejercicio nuevo desde cero.
218
+ """
219
219
  tags = tags or []
220
220
  context = {} # Base implementations doesn't use context
221
221
 
@@ -233,7 +233,7 @@ INSTRUCCIONES:
233
233
  if not provider: return None
234
234
 
235
235
  # 3. Generar
236
- content = provider.generate_content(prompt)
236
+ content = provider.generate_content(prompt, max_tokens=max_tokens)
237
237
  if not content: return None
238
238
 
239
239
  # 4. Parsear
@@ -265,12 +265,12 @@ INSTRUCCIONES:
265
265
  'type': exercise_type
266
266
  }
267
267
 
268
- def generate_variation_with_solution(self, exercise: Dict, analysis: Dict) -> Optional[Dict]:
268
+ def generate_variation_with_solution(self, exercise: Dict, analysis: Dict, max_tokens: int = 2000) -> Optional[Dict]:
269
269
  """
270
270
  Genera una variación con su solución.
271
271
  """
272
272
  # Primero generar el ejercicio
273
- variation = self.generate_variation(exercise, analysis)
273
+ variation = self.generate_variation(exercise, analysis, max_tokens=max_tokens)
274
274
 
275
275
  if not variation:
276
276
  return None
@@ -290,7 +290,7 @@ EJERCICIO:
290
290
 
291
291
  GENERA LA SOLUCIÓN COMPLETA:"""
292
292
 
293
- solution_content = provider.generate_content(solution_prompt)
293
+ solution_content = provider.generate_content(solution_prompt, max_tokens=max_tokens)
294
294
 
295
295
  if solution_content:
296
296
  variation['variation_solution'] = solution_content
@@ -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
@@ -1,17 +1,17 @@
1
- evolutia_cli.py,sha256=1_1JNf1YX465velp9U2-xFYOGTk-hGA-9SMug9Yr15M,8395
1
+ evolutia_cli.py,sha256=-B9615kK6DjGB8RckVvG_o6S_88fE56_RFv4GcRHqx8,8522
2
2
  evolutia/__init__.py,sha256=Fne5WLrmdjIxOSSL6z92G2alAoGRzwGnvpejFqksTiY,350
3
3
  evolutia/async_llm_providers.py,sha256=HVbKmWhg9rGe3FphsLjeil0SD85tk83j8xtw3W5HjZU,5465
4
4
  evolutia/complexity_validator.py,sha256=4BtRvTDVfQUweaz-4QabO-wiEtr5S7_3n0AuAd1Bn5Y,7490
5
5
  evolutia/config_manager.py,sha256=Cf_MQzUq-g5rQuJN_csKt9gJSwPJ6kYg-Dw8uYp1um0,9183
6
- evolutia/evolutia_engine.py,sha256=PDpXPydffrQfT_VUX4BYyW2jqKFIUbO0QVQARk76Yp8,22351
6
+ evolutia/evolutia_engine.py,sha256=-UPEOHydeZCjipmcqHpiJmcCFEiQYGDMO4rx3_KIO2c,22713
7
7
  evolutia/exam_generator.py,sha256=7CKdo86u8bC6yYJY7I3WITcyV_4L8lMRETngGYf2WbU,13837
8
8
  evolutia/exceptions.py,sha256=A1xnYmPlySYEeTHR12ct7Apet2n6JhGugjOYX4mmobw,779
9
9
  evolutia/exercise_analyzer.py,sha256=bY4c-hqeef_xrDTVC9X4MzZlglrhNejWtwEgI_NJ-Mw,8666
10
10
  evolutia/imports.py,sha256=CTsHJ5FukEKMh5GM_O9EsoH61F6xMGj_A3gV0H869Hc,5914
11
- evolutia/llm_providers.py,sha256=TMOVWv9kZcj5Ydp_nBvaJaEohjPdhHPmFPa-VHC2yJA,14955
11
+ evolutia/llm_providers.py,sha256=y3g8n-VMRAn3X0JMt2Ky5h5UEqImXfvYCou7wiuU17c,15505
12
12
  evolutia/material_extractor.py,sha256=CQGgCS9sRBNIhKeR2ya4V7TYBz47yWcEQdjpGEduZRY,12429
13
13
  evolutia/retry_utils.py,sha256=j-DS7MtQaNq2i1Rp788mPVRa_-xs_pEM6PGMzXpICfw,9830
14
- evolutia/variation_generator.py,sha256=ucJL2_OE0fGZeke5BiXItT3vqYLGRo-L-U-i-sHX6Ts,11383
14
+ evolutia/variation_generator.py,sha256=bmH17aeatQFYOqF5C2R9U2220imQhcvELSGAqPtEgr0,11629
15
15
  evolutia/cache/__init__.py,sha256=XAI2hwXc7SMrvs3n8VxgmbVuB7Ma4euQsqdoTuhPRQ0,263
16
16
  evolutia/cache/exercise_cache.py,sha256=eVd0DmcBxoq3eiuOCjiWAZUCtQNIdZBaO6qKqAQ9vjU,7010
17
17
  evolutia/cache/llm_cache.py,sha256=BdUcVeJ8GvAtIZ1s4rEgklcqaXPwORl8VoZp6tdzA_g,16608
@@ -22,16 +22,17 @@ evolutia/rag/enhanced_variation_generator.py,sha256=hFA60OW6-02d8HqluiDTgh7Apwkj
22
22
  evolutia/rag/rag_indexer.py,sha256=Rg-PtvEJXW5IGo-nLh3h1TT51YXnbz9mwoifUTwUiZM,16732
23
23
  evolutia/rag/rag_manager.py,sha256=ODfOiREv8OyQ7WfCdIOPmynJ6grckN6uLYjuECrt3LU,8071
24
24
  evolutia/rag/rag_retriever.py,sha256=It8gB-dL0Ip2r2zCD5Oq_Hmc-C8fbCyrCLaNOghqhLw,14370
25
+ evolutia/schemas/config.schema.json,sha256=GzQxWpJeog5BMYlkEFkzzwk3UNVX0ndDyKRh6tvOafs,6481
25
26
  evolutia/utils/__init__.py,sha256=rD3hl92fM5L3_7QCRrCjyR91bx_bOsw0baVQrCjy6vg,56
26
27
  evolutia/utils/json_parser.py,sha256=71yA15bYCGkZ7pAhLoqoGuKTrFyAsjWDNEgG7rhsloo,3308
27
28
  evolutia/utils/markdown_parser.py,sha256=LjMFMuq4liXaRVXBd6FsH1ZlfiFlO8Hryg-CNAgaYYk,5743
28
29
  evolutia/utils/math_extractor.py,sha256=tKfzwnmj8Puw9w624bBAnRjgxr3my-O3a9UcIyi8XTk,5125
29
- evolutia/validation/__init__.py,sha256=q56nc4RekrPSuOAxJdogBMZHik8Lj69V2SyLD5DfRqg,40
30
+ evolutia/validation/__init__.py,sha256=TxFTXvjfbDaD26gIDy3XE7SEGdzwMV-aocX-OMs_wxE,111
30
31
  evolutia/validation/args_validator.py,sha256=lhlQ5_swUBYTO_7MXUgTgtwd_JLCCEjvh4YxM7KbtkE,10051
31
32
  evolutia/validation/config_validator.py,sha256=ATTI0--zZIG85WzASU59M2nW-nZbNSxWOe82QJ1vIMQ,20968
32
- evolutia-0.1.3.dist-info/licenses/LICENSE,sha256=jyOI5zt59oNBGE33KX2L1py7ZwWKqVFNcgEePR6o6YA,11564
33
- evolutia-0.1.3.dist-info/METADATA,sha256=Vr_Zb7mokCyTSmcC-P0K25Zu0-NYWIqYcFMvBIsOT34,16955
34
- evolutia-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
35
- evolutia-0.1.3.dist-info/entry_points.txt,sha256=BMYhya9XcbrfsjGfp4lMVQF0TK3Bd_Kj6mZtFhVJ7zo,47
36
- evolutia-0.1.3.dist-info/top_level.txt,sha256=GrIvEW8qAI8CIW5XTPnCqC7o06qJIL8p9CGwVhCEC4s,22
37
- evolutia-0.1.3.dist-info/RECORD,,
33
+ evolutia-0.1.5.dist-info/licenses/LICENSE,sha256=jyOI5zt59oNBGE33KX2L1py7ZwWKqVFNcgEePR6o6YA,11564
34
+ evolutia-0.1.5.dist-info/METADATA,sha256=VUgcMYe8hYZmGneid3GEjcveiijXnTW6zrvOWIxwYz8,16955
35
+ evolutia-0.1.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
36
+ evolutia-0.1.5.dist-info/entry_points.txt,sha256=BMYhya9XcbrfsjGfp4lMVQF0TK3Bd_Kj6mZtFhVJ7zo,47
37
+ evolutia-0.1.5.dist-info/top_level.txt,sha256=GrIvEW8qAI8CIW5XTPnCqC7o06qJIL8p9CGwVhCEC4s,22
38
+ evolutia-0.1.5.dist-info/RECORD,,
evolutia_cli.py CHANGED
@@ -12,12 +12,12 @@ from dotenv import load_dotenv
12
12
  # Cargar variables de entorno desde el directorio actual (donde el usuario ejecuta el comando)
13
13
  load_dotenv()
14
14
 
15
- # Add current directory to path so we can import 'evolutia' package if running locally without install
16
- sys.path.insert(0, str(Path(__file__).parent))
17
-
18
- from evolutia.evolutia_engine import EvolutiaEngine
19
- from evolutia.config_manager import ConfigManager
20
- from evolutia.validation.args_validator import ArgsValidator, ValidationError
15
+ # Add current directory to path so we can import 'evolutia' package if running locally without install
16
+ sys.path.insert(0, str(Path(__file__).parent))
17
+
18
+ from evolutia.evolutia_engine import EvolutiaEngine
19
+ from evolutia.config_manager import ConfigManager
20
+ from evolutia.validation.args_validator import ArgsValidator, ValidationError
21
21
 
22
22
  # Configurar logging
23
23
  logging.basicConfig(
@@ -64,20 +64,21 @@ Ejemplos:
64
64
  parser.add_argument('--query', type=str, help='Consulta RAG')
65
65
  parser.add_argument('--workers', type=int, default=5, help='Número de hilos para generación paralela')
66
66
  parser.add_argument('--analyze', action='store_true', help='Analizar estructura y generar config.yaml')
67
+ parser.add_argument('--max_tokens', type=int, default=2000, help='Máximo de tokens para generación')
68
+
69
+ args = parser.parse_args()
70
+
71
+ # Validar argumentos con ArgsValidator
72
+ validator = ArgsValidator()
73
+ is_valid, errors = validator.validate_args(args)
74
+
75
+ if not is_valid:
76
+ logger.error("Errores de validación en argumentos:")
77
+ for error in errors:
78
+ logger.error(f" - {error}")
79
+ parser.error("\n".join(errors))
67
80
 
68
- args = parser.parse_args()
69
-
70
- # Validar argumentos con ArgsValidator
71
- validator = ArgsValidator()
72
- is_valid, errors = validator.validate_args(args)
73
-
74
- if not is_valid:
75
- logger.error("Errores de validación en argumentos:")
76
- for error in errors:
77
- logger.error(f" - {error}")
78
- parser.error("\n".join(errors))
79
-
80
- if args.analyze:
81
+ if args.analyze:
81
82
  print("Analizando estructura del proyecto...")
82
83
  ConfigManager(str(Path.cwd())).update_config_from_structure()
83
84
  return 0