evolutia 0.1.1__py3-none-any.whl → 0.1.2__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 +54 -91
- 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.2.dist-info/METADATA +536 -0
- evolutia-0.1.2.dist-info/RECORD +37 -0
- 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.2.dist-info}/WHEEL +0 -0
- {evolutia-0.1.1.dist-info → evolutia-0.1.2.dist-info}/entry_points.txt +0 -0
- {evolutia-0.1.1.dist-info → evolutia-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {evolutia-0.1.1.dist-info → evolutia-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validador de argumentos CLI para EvolutIA.
|
|
3
|
+
Valida exhaustivamente los argumentos pasados por línea de comandos.
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Tuple, Optional, Set
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ValidationError(Exception):
|
|
14
|
+
"""Excepción para errores de validación."""
|
|
15
|
+
def __init__(self, message: str, errors: Optional[List[str]] = None):
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.errors = errors or []
|
|
18
|
+
self.message = message
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ArgsValidator:
|
|
22
|
+
"""Validador de argumentos de línea de comandos."""
|
|
23
|
+
|
|
24
|
+
# Valores válidos para algunos argumentos
|
|
25
|
+
VALID_COMPLEXITY = {'media', 'alta', 'muy_alta'}
|
|
26
|
+
VALID_API_PROVIDERS = {
|
|
27
|
+
'openai', 'anthropic', 'local', 'gemini', 'deepseek', 'generic'
|
|
28
|
+
}
|
|
29
|
+
VALID_MODES = {'variation', 'creation'}
|
|
30
|
+
VALID_EXERCISE_TYPES = {'development', 'multiple_choice'}
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self.errors: List[str] = []
|
|
34
|
+
self.warnings: List[str] = []
|
|
35
|
+
|
|
36
|
+
def validate_args(self, args: argparse.Namespace) -> Tuple[bool, List[str]]:
|
|
37
|
+
"""
|
|
38
|
+
Valida todos los argumentos CLI.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
args: Objeto argparse.Namespace con los argumentos
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tupla (is_valid, error_messages) donde is_valid es True si todos
|
|
45
|
+
los argumentos son válidos, y error_messages es una lista con
|
|
46
|
+
mensajes de error (vacía si is_valid es True)
|
|
47
|
+
"""
|
|
48
|
+
self.errors = []
|
|
49
|
+
self.warnings = []
|
|
50
|
+
|
|
51
|
+
# Validaciones generales
|
|
52
|
+
self._validate_complejidad(args)
|
|
53
|
+
self._validate_num_ejercicios(args)
|
|
54
|
+
self._validate_api(args)
|
|
55
|
+
self._validate_workers(args)
|
|
56
|
+
self._validate_mode(args)
|
|
57
|
+
self._validate_exercise_type(args)
|
|
58
|
+
|
|
59
|
+
# Validaciones de rutas
|
|
60
|
+
self._validate_base_path(args)
|
|
61
|
+
self._validate_config_path(args)
|
|
62
|
+
self._validate_output_path(args)
|
|
63
|
+
|
|
64
|
+
# Validaciones de combinaciones
|
|
65
|
+
self._validate_mode_combinations(args)
|
|
66
|
+
self._validate_rag_combinations(args)
|
|
67
|
+
|
|
68
|
+
# Validaciones específicas de modos
|
|
69
|
+
self._validate_variation_mode(args)
|
|
70
|
+
self._validate_creation_mode(args)
|
|
71
|
+
|
|
72
|
+
# Log warnings
|
|
73
|
+
for warning in self.warnings:
|
|
74
|
+
logger.warning(f"[ArgsValidator] {warning}")
|
|
75
|
+
|
|
76
|
+
return len(self.errors) == 0, self.errors
|
|
77
|
+
|
|
78
|
+
def _validate_complejidad(self, args: argparse.Namespace):
|
|
79
|
+
"""Valida que el nivel de complejidad sea válido."""
|
|
80
|
+
if hasattr(args, 'complejidad') and args.complejidad:
|
|
81
|
+
if args.complejidad not in self.VALID_COMPLEXITY:
|
|
82
|
+
self.errors.append(
|
|
83
|
+
f"--complejidad debe ser uno de {sorted(self.VALID_COMPLEXITY)}, "
|
|
84
|
+
f"obtenido: {args.complejidad}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _validate_num_ejercicios(self, args: argparse.Namespace):
|
|
88
|
+
"""Valida que el número de ejercicios sea positivo y razonable."""
|
|
89
|
+
if hasattr(args, 'num_ejercicios') and args.num_ejercicios is not None:
|
|
90
|
+
if args.num_ejercicios <= 0:
|
|
91
|
+
self.errors.append(
|
|
92
|
+
f"--num_ejercicios debe ser positivo, obtenido: {args.num_ejercicios}"
|
|
93
|
+
)
|
|
94
|
+
elif args.num_ejercicios > 50:
|
|
95
|
+
self.warnings.append(
|
|
96
|
+
f"--num_ejercicios es muy alto ({args.num_ejercicios}), "
|
|
97
|
+
"esto puede generar un costo significativo en API"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _validate_api(self, args: argparse.Namespace):
|
|
101
|
+
"""Valida que el proveedor de API sea válido."""
|
|
102
|
+
if hasattr(args, 'api') and args.api:
|
|
103
|
+
if args.api not in self.VALID_API_PROVIDERS:
|
|
104
|
+
self.errors.append(
|
|
105
|
+
f"--api debe ser uno de {sorted(self.VALID_API_PROVIDERS)}, "
|
|
106
|
+
f"obtenido: {args.api}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Validaciones específicas por proveedor
|
|
110
|
+
if args.api == 'generic' and not getattr(args, 'base_url', None):
|
|
111
|
+
self.warnings.append(
|
|
112
|
+
"--api generic requiere --base_url, usando valor por defecto"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _validate_workers(self, args: argparse.Namespace):
|
|
116
|
+
"""Valida que el número de workers esté en un rango razonable."""
|
|
117
|
+
if hasattr(args, 'workers') and args.workers is not None:
|
|
118
|
+
if args.workers < 1:
|
|
119
|
+
self.errors.append(
|
|
120
|
+
f"--workers debe ser al menos 1, obtenido: {args.workers}"
|
|
121
|
+
)
|
|
122
|
+
elif args.workers > 20:
|
|
123
|
+
self.warnings.append(
|
|
124
|
+
f"--workers es alto ({args.workers}), "
|
|
125
|
+
"esto puede causar rate limiting de API"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _validate_mode(self, args: argparse.Namespace):
|
|
129
|
+
"""Valida que el modo de operación sea válido."""
|
|
130
|
+
if hasattr(args, 'mode') and args.mode:
|
|
131
|
+
if args.mode not in self.VALID_MODES:
|
|
132
|
+
self.errors.append(
|
|
133
|
+
f"--mode debe ser uno de {sorted(self.VALID_MODES)}, "
|
|
134
|
+
f"obtenido: {args.mode}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _validate_exercise_type(self, args: argparse.Namespace):
|
|
138
|
+
"""Valida que el tipo de ejercicio sea válido."""
|
|
139
|
+
if hasattr(args, 'type') and args.type:
|
|
140
|
+
if args.type not in self.VALID_EXERCISE_TYPES:
|
|
141
|
+
self.errors.append(
|
|
142
|
+
f"--type debe ser uno de {sorted(self.VALID_EXERCISE_TYPES)}, "
|
|
143
|
+
f"obtenido: {args.type}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _validate_base_path(self, args: argparse.Namespace):
|
|
147
|
+
"""Valida que la ruta base exista."""
|
|
148
|
+
if hasattr(args, 'base_path') and args.base_path:
|
|
149
|
+
base_path = Path(args.base_path)
|
|
150
|
+
if not base_path.exists():
|
|
151
|
+
self.errors.append(
|
|
152
|
+
f"--base_path no existe: {args.base_path}"
|
|
153
|
+
)
|
|
154
|
+
elif not base_path.is_dir():
|
|
155
|
+
self.errors.append(
|
|
156
|
+
f"--base_path no es un directorio: {args.base_path}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _validate_config_path(self, args: argparse.Namespace):
|
|
160
|
+
"""Valida que la ruta del archivo de configuración exista si se especifica."""
|
|
161
|
+
if hasattr(args, 'config') and args.config:
|
|
162
|
+
config_path = Path(args.config)
|
|
163
|
+
if not config_path.exists():
|
|
164
|
+
self.errors.append(
|
|
165
|
+
f"--config no existe: {args.config}"
|
|
166
|
+
)
|
|
167
|
+
elif not config_path.is_file():
|
|
168
|
+
self.errors.append(
|
|
169
|
+
f"--config no es un archivo: {args.config}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _validate_output_path(self, args: argparse.Namespace):
|
|
173
|
+
"""Valida que el directorio de salida pueda crearse."""
|
|
174
|
+
if hasattr(args, 'output') and args.output:
|
|
175
|
+
output_path = Path(args.output)
|
|
176
|
+
parent = output_path.parent
|
|
177
|
+
|
|
178
|
+
if parent.exists() and not parent.is_dir():
|
|
179
|
+
self.errors.append(
|
|
180
|
+
f"--output no es un directorio válido: {parent}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Verificar permisos de escritura en el directorio padre
|
|
184
|
+
if parent.exists() and parent.is_dir():
|
|
185
|
+
try:
|
|
186
|
+
test_file = parent / '.evolutia_write_test'
|
|
187
|
+
test_file.touch()
|
|
188
|
+
test_file.unlink()
|
|
189
|
+
except (PermissionError, OSError) as e:
|
|
190
|
+
self.errors.append(
|
|
191
|
+
f"--output no tiene permisos de escritura: {parent} ({e})"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _validate_mode_combinations(self, args: argparse.Namespace):
|
|
195
|
+
"""Valida que las combinaciones de modos sean válidas."""
|
|
196
|
+
is_exclusive_mode = getattr(args, 'analyze', False) or \
|
|
197
|
+
getattr(args, 'list', False) or \
|
|
198
|
+
getattr(args, 'query', False) or \
|
|
199
|
+
getattr(args, 'reindex', False)
|
|
200
|
+
|
|
201
|
+
# Modos exclusivos no requieren tema ni output
|
|
202
|
+
if is_exclusive_mode:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
# Modos normales requieren tema o label
|
|
206
|
+
if not getattr(args, 'tema', None) and not getattr(args, 'label', None):
|
|
207
|
+
# Este error ya está en el CLI, pero lo verificamos por completitud
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
# Modos normales requieren output
|
|
211
|
+
if not getattr(args, 'output', None):
|
|
212
|
+
# Este error ya está en el CLI, pero lo verificamos por completitud
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
def _validate_rag_combinations(self, args: argparse.Namespace):
|
|
216
|
+
"""Valida las combinaciones de opciones RAG."""
|
|
217
|
+
use_rag = getattr(args, 'use_rag', False)
|
|
218
|
+
query = getattr(args, 'query', None)
|
|
219
|
+
reindex = getattr(args, 'reindex', False)
|
|
220
|
+
|
|
221
|
+
if not (use_rag or query or reindex):
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# RAG requiere dependencies opcionales
|
|
225
|
+
try:
|
|
226
|
+
import chromadb
|
|
227
|
+
from sentence_transformers import SentenceTransformer
|
|
228
|
+
except ImportError as e:
|
|
229
|
+
self.warnings.append(
|
|
230
|
+
f"RAG solicitado pero faltan dependencias: {e}. "
|
|
231
|
+
"Instala con: pip install -e '.[rag]'"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _validate_variation_mode(self, args: argparse.Namespace):
|
|
235
|
+
"""Valida requisitos específicos para modo variation."""
|
|
236
|
+
if getattr(args, 'mode', None) == 'variation':
|
|
237
|
+
# Mode variation requiere ejercicios existentes
|
|
238
|
+
# (esto se verifica más tarde en el engine)
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
def _validate_creation_mode(self, args: argparse.Namespace):
|
|
242
|
+
"""Valida requisitos específicos para modo creation."""
|
|
243
|
+
if getattr(args, 'mode', None) == 'creation':
|
|
244
|
+
# Mode creation requiere tema y tags
|
|
245
|
+
if not getattr(args, 'tema', None):
|
|
246
|
+
self.errors.append(
|
|
247
|
+
"Mode creation requiere --tema"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if not getattr(args, 'tags', None):
|
|
251
|
+
self.warnings.append(
|
|
252
|
+
"Mode creation sin --tags puede generar ejercicios genéricos"
|
|
253
|
+
)
|