evolutia 0.1.0__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 +10 -1
- 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 +60 -41
- 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 +224 -60
- 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 +252 -50
- evolutia-0.1.2.dist-info/METADATA +536 -0
- evolutia-0.1.2.dist-info/RECORD +37 -0
- {evolutia-0.1.0.dist-info → evolutia-0.1.2.dist-info}/WHEEL +1 -1
- {evolutia-0.1.0.dist-info → evolutia-0.1.2.dist-info}/licenses/LICENSE +1 -1
- evolutia_cli.py +30 -7
- evolutia-0.1.0.dist-info/METADATA +0 -723
- evolutia-0.1.0.dist-info/RECORD +0 -27
- {evolutia-0.1.0.dist-info → evolutia-0.1.2.dist-info}/entry_points.txt +0 -0
- {evolutia-0.1.0.dist-info → evolutia-0.1.2.dist-info}/top_level.txt +0 -0
evolutia/variation_generator.py
CHANGED
|
@@ -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
|
|
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
|
|
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,28 +37,234 @@ class VariationGenerator:
|
|
|
37
37
|
|
|
38
38
|
self._provider_instance = None
|
|
39
39
|
|
|
40
|
-
def _get_provider(self):
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
|
|
71
|
+
def _create_prompt(self, exercise: Dict, analysis: Dict) -> str:
|
|
72
|
+
"""Crea el prompt para generar una variación."""
|
|
73
|
+
|
|
74
|
+
content = exercise.get('content', '')
|
|
75
|
+
complexity = analysis.get('total_complexity', 0)
|
|
76
|
+
concepts = ", ".join(analysis.get('concepts', []))
|
|
77
|
+
|
|
78
|
+
prompt = f"""Actúa como un profesor experto de física y matemáticas universitarias.
|
|
79
|
+
Tu tarea es crear una VARIACIÓN de un ejercicio existente.
|
|
80
|
+
|
|
81
|
+
EJERCICIO ORIGINAL:
|
|
82
|
+
{content}
|
|
83
|
+
|
|
84
|
+
ANÁLISIS DE COMPLEJIDAD ORIGINAL:
|
|
85
|
+
- Complejidad: {complexity:.2f}
|
|
86
|
+
- Conceptos: {concepts}
|
|
87
|
+
|
|
88
|
+
OBJETIVO:
|
|
89
|
+
Generar una nueva versión del ejercicio que sea MÁS COMPLEJA y DESAFIANTE, pero evaluando los mismos principios fundamentales.
|
|
90
|
+
|
|
91
|
+
ESTRATEGIAS PARA AUMENTAR COMPLEJIDAD:
|
|
92
|
+
1. Cambia las variables numéricas por parámetros simbólicos (a, b, R, etc.)
|
|
93
|
+
2. Introduce sistemas de coordenadas diferentes (cilíndricas/esféricas) si aplica
|
|
94
|
+
3. Combina múltiples conceptos en un solo problema
|
|
95
|
+
4. Agrega una restricción o condición de borde adicional
|
|
96
|
+
5. Pide una generalización del resultado
|
|
97
|
+
|
|
98
|
+
REGLAS DE FORMATO:
|
|
99
|
+
1. Usa Markdown estándar
|
|
100
|
+
2. Usa LaTeX para matemáticas (bloques :::math o $$...$$)
|
|
101
|
+
3. La salida debe contener DOS PARTES separadas por "SOLUCIÓN REQUERIDA:"
|
|
102
|
+
- Parte 1: El enunciado del nuevo ejercicio (encabezado con "EJERCICIO VARIADO:")
|
|
103
|
+
- Parte 2: La solución paso a paso
|
|
104
|
+
|
|
105
|
+
Genera solo el contenido solicitado."""
|
|
106
|
+
return prompt
|
|
107
|
+
|
|
108
|
+
def _create_quiz_prompt(self, context_info: Dict[str, Any]) -> str:
|
|
109
|
+
"""Crea prompt para ejercicios de selección única."""
|
|
110
|
+
content = context_info.get('content', '')
|
|
111
|
+
|
|
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:
|
|
113
|
+
|
|
114
|
+
MATERIAL BASE:
|
|
115
|
+
{content}
|
|
116
|
+
|
|
117
|
+
REQUISITOS:
|
|
118
|
+
1. La pregunta debe ser conceptual y desafiante.
|
|
119
|
+
2. Genera 4 opciones (A, B, C, D).
|
|
120
|
+
3. Solo una opción debe ser correcta, las otras deben ser distractores plausibles.
|
|
121
|
+
4. Devuelve la respuesta EXCLUSIVAMENTE en formato JSON válido:
|
|
122
|
+
{{
|
|
123
|
+
"question": "Enunciado de la pregunta en Markdown...",
|
|
124
|
+
"options": {{
|
|
125
|
+
"A": "Texto opción A",
|
|
126
|
+
"B": "Texto opción B",
|
|
127
|
+
"C": "Texto opción C",
|
|
128
|
+
"D": "Texto opción D"
|
|
129
|
+
}},
|
|
130
|
+
"correct_option": "A",
|
|
131
|
+
"explanation": "Explicación detallada de por qué es la correcta..."
|
|
132
|
+
}}
|
|
133
|
+
"""
|
|
134
|
+
return prompt
|
|
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
|
|
163
|
+
variation_content = ""
|
|
164
|
+
variation_solution = ""
|
|
165
|
+
|
|
166
|
+
if exercise_type == 'multiple_choice':
|
|
167
|
+
data = extract_and_parse_json(content)
|
|
168
|
+
if data and 'question' in data:
|
|
169
|
+
variation_content = f"{data['question']}\n\n"
|
|
170
|
+
for opt, text in data.get('options', {}).items():
|
|
171
|
+
variation_content += f"- **{opt})** {text}\n"
|
|
172
|
+
variation_solution = f"**Respuesta Correcta: {data.get('correct_option', '?')}**\n\n{data.get('explanation', '')}"
|
|
173
|
+
else:
|
|
174
|
+
variation_content = content
|
|
175
|
+
variation_solution = "Error parseando JSON de quiz."
|
|
176
|
+
else:
|
|
177
|
+
# Parseo texto plano
|
|
178
|
+
parts = content.split("SOLUCIÓN REQUERIDA:")
|
|
179
|
+
if len(parts) == 2:
|
|
180
|
+
variation_content = parts[0].replace("EJERCICIO VARIADO:", "").strip()
|
|
181
|
+
variation_solution = parts[1].strip()
|
|
182
|
+
else:
|
|
183
|
+
variation_content = content
|
|
184
|
+
variation_solution = ""
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
'variation_content': variation_content,
|
|
188
|
+
'variation_solution': variation_solution,
|
|
189
|
+
'original_frontmatter': exercise.get('frontmatter', {}),
|
|
190
|
+
'original_label': exercise.get('label'),
|
|
191
|
+
'type': exercise_type
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def _create_new_exercise_prompt(self, topic: str, tags: list, context: Dict, difficulty: str) -> str:
|
|
195
|
+
"""Crea prompt para ejercicio nuevo desde cero."""
|
|
196
|
+
tags_str = ", ".join(tags)
|
|
197
|
+
|
|
198
|
+
prompt = f"""Diseña un NUEVO ejercicio de examen universitario para:
|
|
199
|
+
Asignatura/Tema: {topic}
|
|
200
|
+
Conceptos Clave (Tags): {tags_str}
|
|
201
|
+
Nivel de Dificultad: {difficulty.upper()} (donde ALTA implica demostraciones o conexiones no triviales).
|
|
202
|
+
|
|
203
|
+
INSTRUCCIONES:
|
|
204
|
+
1. Crea un problema original que evalúe comprensión profunda.
|
|
205
|
+
2. No copies ejercicios de libros de texto.
|
|
206
|
+
3. Formato de salida:
|
|
207
|
+
EJERCICIO NUEVO:
|
|
208
|
+
[Enunciado en Markdown con LaTeX]
|
|
209
|
+
|
|
210
|
+
SOLUCIÓN REQUERIDA:
|
|
211
|
+
[Solución paso a paso]
|
|
212
|
+
"""
|
|
213
|
+
return prompt
|
|
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
|
+
"""
|
|
219
|
+
tags = tags or []
|
|
220
|
+
context = {} # Base implementations doesn't use context
|
|
221
|
+
|
|
222
|
+
# 1. Crear prompt
|
|
223
|
+
if exercise_type == 'multiple_choice':
|
|
224
|
+
context_info = {
|
|
225
|
+
'content': f"Tema: {topic}\nTags: {', '.join(tags)}\nDificultad: {difficulty}"
|
|
226
|
+
}
|
|
227
|
+
prompt = self._create_quiz_prompt(context_info)
|
|
228
|
+
else:
|
|
229
|
+
prompt = self._create_new_exercise_prompt(topic, tags, context, difficulty)
|
|
50
230
|
|
|
51
|
-
|
|
52
|
-
|
|
231
|
+
# 2. Get Provider
|
|
232
|
+
provider = self._get_provider()
|
|
233
|
+
if not provider: return None
|
|
234
|
+
|
|
235
|
+
# 3. Generar
|
|
236
|
+
content = provider.generate_content(prompt)
|
|
237
|
+
if not content: return None
|
|
238
|
+
|
|
239
|
+
# 4. Parsear
|
|
240
|
+
# Reutilizamos lógica simple de parseo
|
|
241
|
+
if exercise_type == 'multiple_choice':
|
|
242
|
+
data = extract_and_parse_json(content)
|
|
243
|
+
if data and 'question' in data:
|
|
244
|
+
var_content = f"{data['question']}\n\n"
|
|
245
|
+
# ... (simplificado, igual que arriba)
|
|
246
|
+
for k, v in data.get('options',{}).items():
|
|
247
|
+
var_content += f"- **{k})** {v}\n"
|
|
248
|
+
var_sol = f"R: {data.get('correct_option')}. {data.get('explanation')}"
|
|
249
|
+
else:
|
|
250
|
+
var_content = content
|
|
251
|
+
var_sol = ""
|
|
252
|
+
else:
|
|
253
|
+
parts = content.split("SOLUCIÓN REQUERIDA:")
|
|
254
|
+
if len(parts) == 2:
|
|
255
|
+
var_content = parts[0].replace("EJERCICIO NUEVO:", "").strip()
|
|
256
|
+
var_sol = parts[1].strip()
|
|
257
|
+
else:
|
|
258
|
+
var_content = content
|
|
259
|
+
var_sol = ""
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
'variation_content': var_content,
|
|
263
|
+
'variation_solution': var_sol,
|
|
264
|
+
'original_frontmatter': {'topic': topic, 'tags': tags},
|
|
265
|
+
'type': exercise_type
|
|
266
|
+
}
|
|
53
267
|
|
|
54
|
-
try:
|
|
55
|
-
self._provider_instance = get_provider(self.api_provider, **kwargs)
|
|
56
|
-
except ValueError as e:
|
|
57
|
-
logger.error(f"Error inicializando proveedor: {e}")
|
|
58
|
-
return None
|
|
59
|
-
|
|
60
|
-
return self._provider_instance
|
|
61
|
-
|
|
62
268
|
def generate_variation_with_solution(self, exercise: Dict, analysis: Dict) -> Optional[Dict]:
|
|
63
269
|
"""
|
|
64
270
|
Genera una variación con su solución.
|
|
@@ -69,23 +275,19 @@ class VariationGenerator:
|
|
|
69
275
|
if not variation:
|
|
70
276
|
return None
|
|
71
277
|
|
|
278
|
+
# Si ya tiene solución (porque el prompt único la pidió), retornarla
|
|
279
|
+
if variation.get('variation_solution'):
|
|
280
|
+
return variation
|
|
281
|
+
|
|
72
282
|
provider = self._get_provider()
|
|
73
283
|
if not provider: return None
|
|
74
284
|
|
|
75
|
-
#
|
|
76
|
-
solution_prompt = f"""Eres un experto en métodos matemáticos
|
|
77
|
-
|
|
285
|
+
# Si no, generar la solución por separado (fallback legacy)
|
|
286
|
+
solution_prompt = f"""Eres un experto en métodos matemáticos. Resuelve el siguiente ejercicio paso a paso:
|
|
287
|
+
|
|
78
288
|
EJERCICIO:
|
|
79
289
|
{variation['variation_content']}
|
|
80
290
|
|
|
81
|
-
INSTRUCCIONES:
|
|
82
|
-
1. Resuelve el ejercicio de forma completa y detallada
|
|
83
|
-
2. Muestra todos los pasos intermedios
|
|
84
|
-
3. Usa notación matemática LaTeX correcta
|
|
85
|
-
4. Explica el razonamiento cuando sea necesario
|
|
86
|
-
5. Usa bloques :::{{math}} para ecuaciones display y $...$ para inline
|
|
87
|
-
6. Escribe en español
|
|
88
|
-
|
|
89
291
|
GENERA LA SOLUCIÓN COMPLETA:"""
|
|
90
292
|
|
|
91
293
|
solution_content = provider.generate_content(solution_prompt)
|