cesar commited on
Commit
0dfae1c
·
verified ·
1 Parent(s): b7193be

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +94 -67
app.py CHANGED
@@ -4,7 +4,7 @@ import os
4
  import re
5
  import vertexai
6
  from vertexai.generative_models import GenerativeModel, Part, SafetySetting
7
- from difflib import SequenceMatcher # Para comparar similitud
8
 
9
  # --------------------
10
  # CONFIGURACIÓN GLOBAL
@@ -53,7 +53,7 @@ def extraer_texto(pdf_path: str) -> str:
53
  return texto_total
54
 
55
  # -----------
56
- # PARSEO TEXTO
57
  # -----------
58
  def split_secciones(texto: str) -> (str, str):
59
  """
@@ -97,128 +97,153 @@ def parsear_enumeraciones(texto: str) -> dict:
97
  return resultado
98
 
99
  # ------------
100
- # COMPARACIÓN
101
  # ------------
102
  def similar_textos(texto1: str, texto2: str) -> float:
103
  """Calcula la similitud entre dos textos (valor entre 0 y 1)."""
104
  return SequenceMatcher(None, texto1, texto2).ratio()
105
 
106
- def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> str:
107
  """
108
- Compara dict_docente vs dict_alumno y retorna retroalimentación.
109
- - Si la 'Pregunta X' no está en dict_alumno, se recomienda revisar el tema.
110
- - Si está, se compara la respuesta del alumno con la correcta.
111
- Se eliminan los saltos de línea en la respuesta del alumno.
 
 
 
 
 
 
 
112
  """
113
- retroalimentacion = []
 
114
  for pregunta, resp_correcta in dict_docente.items():
115
- resp_alumno = dict_alumno.get(pregunta, None)
116
- if resp_alumno is None or resp_alumno.strip() == "":
117
- retroalimentacion.append(
 
 
 
118
  f"**{pregunta}**\n"
119
  f"Respuesta del alumno: No fue asignada.\n"
120
- f"Respuesta correcta: {' '.join(resp_correcta.split())}\n"
121
- f"Recomendación: Revisar el tema correspondiente.\n"
122
  )
 
 
123
  else:
124
- # Eliminar saltos de línea y espacios extra
125
- resp_alumno_clean = " ".join(resp_alumno.split())
126
- resp_correcta_clean = " ".join(resp_correcta.split())
127
- ratio = similar_textos(resp_alumno_clean.lower(), resp_correcta_clean.lower())
128
- if ratio >= 0.8:
129
- feedback_text = "La respuesta es correcta."
 
 
130
  else:
131
- feedback_text = "La respuesta no coincide completamente. Se recomienda revisar la explicación y reforzar el concepto."
132
- retroalimentacion.append(
 
133
  f"**{pregunta}**\n"
134
- f"Respuesta del alumno: {resp_alumno_clean}\n"
135
- f"Respuesta correcta: {resp_correcta_clean}\n"
136
- f"{feedback_text}\n"
137
  )
138
- return "\n".join(retroalimentacion)
 
139
 
140
  # -----------
141
- # FUNCIÓN LÓGICA
142
  # -----------
143
  def revisar_examen(json_cred, pdf_docente, pdf_alumno):
144
  """
145
- Función generadora que muestra progreso en Gradio con yield.
146
- Realiza los siguientes pasos:
147
  1. Configura credenciales.
148
- 2. Extrae texto de los PDFs.
149
- 3. Separa secciones 'Preguntas' y 'RESPUESTAS'.
150
- 4. Parsea las enumeraciones.
151
- 5. Compara las respuestas y genera retroalimentación con recomendaciones.
152
- 6. Llama a un LLM para generar un resumen final.
 
 
153
  """
154
  yield "Cargando credenciales..."
155
  try:
156
  configurar_credenciales(json_cred.name)
157
-
158
  yield "Inicializando Vertex AI..."
159
  vertexai.init(project="deploygpt", location="us-central1")
160
-
161
  yield "Extrayendo texto del PDF del docente..."
162
  texto_docente = extraer_texto(pdf_docente.name)
163
-
164
  yield "Extrayendo texto del PDF del alumno..."
165
  texto_alumno = extraer_texto(pdf_alumno.name)
166
-
167
  yield "Dividiendo secciones (docente)..."
168
  preguntas_doc, respuestas_doc = split_secciones(texto_docente)
169
-
170
  yield "Dividiendo secciones (alumno)..."
171
  preguntas_alum, respuestas_alum = split_secciones(texto_alumno)
172
-
173
  yield "Parseando enumeraciones (docente)..."
174
  dict_preg_doc = parsear_enumeraciones(preguntas_doc)
175
  dict_resp_doc = parsear_enumeraciones(respuestas_doc)
176
-
177
- # Unir preguntas y respuestas del docente
178
  dict_docente = {}
179
- for key_preg in dict_preg_doc:
180
- resp_doc = dict_resp_doc.get(key_preg, "")
181
- dict_docente[key_preg] = resp_doc
182
-
183
  yield "Parseando enumeraciones (alumno)..."
184
  dict_preg_alum = parsear_enumeraciones(preguntas_alum)
185
  dict_resp_alum = parsear_enumeraciones(respuestas_alum)
186
-
187
- # Unir preguntas y respuestas del alumno
188
  dict_alumno = {}
189
- for key_preg in dict_preg_alum:
190
- resp_alum = dict_resp_alum.get(key_preg, "")
191
- dict_alumno[key_preg] = resp_alum
192
-
193
  yield "Comparando preguntas y respuestas..."
194
- feedback = comparar_preguntas_respuestas(dict_docente, dict_alumno)
195
-
196
- if len(feedback.strip()) < 5:
197
  yield "No se encontraron preguntas o respuestas válidas."
198
  return
 
 
 
 
 
 
 
 
199
 
 
 
 
 
 
 
 
200
  yield "Generando resumen final con LLM..."
201
- # Llamada final al LLM:
202
  model = GenerativeModel(
203
  "gemini-1.5-pro-001",
204
- system_instruction=["Eres un profesor experto de bioquímica. No inventes preguntas."]
205
  )
206
- summary_prompt = f"""
207
- Comparación de preguntas y respuestas:
208
- {feedback}
209
- Por favor, genera un breve resumen del desempeño del alumno, indicando si entiende los conceptos y recomendando reforzar los puntos necesarios.
210
- """
211
- summary_part = Part.from_text(summary_prompt)
212
  summary_resp = model.generate_content(
213
  [summary_part],
214
  generation_config=generation_config,
215
  safety_settings=safety_settings,
216
  stream=False
217
  )
218
- final_result = f"{feedback}\n\n**Resumen**\n{summary_resp.text.strip()}"
219
-
 
220
  yield final_result
221
-
222
  except Exception as e:
223
  yield f"Error al procesar: {str(e)}"
224
 
@@ -237,8 +262,10 @@ interface = gr.Interface(
237
  description=(
238
  "Sube las credenciales, el PDF del docente (con las preguntas y respuestas correctas) y el PDF del alumno. "
239
  "El sistema separa las secciones 'Preguntas' y 'RESPUESTAS', parsea las enumeraciones y luego compara las respuestas. "
240
- "Finalmente, se genera un resumen con recomendaciones para reforzar los conceptos según el desempeño del alumno."
 
241
  )
242
  )
243
 
244
  interface.launch(debug=True)
 
 
4
  import re
5
  import vertexai
6
  from vertexai.generative_models import GenerativeModel, Part, SafetySetting
7
+ from difflib import SequenceMatcher
8
 
9
  # --------------------
10
  # CONFIGURACIÓN GLOBAL
 
53
  return texto_total
54
 
55
  # -----------
56
+ # PARSEO DE TEXTO
57
  # -----------
58
  def split_secciones(texto: str) -> (str, str):
59
  """
 
97
  return resultado
98
 
99
  # ------------
100
+ # COMPARACIÓN Y ANÁLISIS
101
  # ------------
102
  def similar_textos(texto1: str, texto2: str) -> float:
103
  """Calcula la similitud entre dos textos (valor entre 0 y 1)."""
104
  return SequenceMatcher(None, texto1, texto2).ratio()
105
 
106
+ def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> (str, list):
107
  """
108
+ Compara las respuestas del docente (correctas) con las del alumno.
109
+ Para cada pregunta:
110
+ - Si no fue asignada se indica "No fue asignada".
111
+ - Si fue asignada se calcula la similitud y se evalúa:
112
+ * Correcta: ratio >= 0.85
113
+ * Incompleta: 0.5 <= ratio < 0.85
114
+ * Incorrecta: ratio < 0.5
115
+ Devuelve:
116
+ - Un string con la retroalimentación por pregunta.
117
+ - Una lista de diccionarios con el análisis por pregunta (para la conclusión).
118
+ Solo se incluyen las preguntas que fueron asignadas al alumno.
119
  """
120
+ feedback = []
121
+ analisis = []
122
  for pregunta, resp_correcta in dict_docente.items():
123
+ # Se “limpian” los textos para eliminar saltos de línea y espacios de más.
124
+ correct_clean = " ".join(resp_correcta.split())
125
+ resp_alumno_raw = dict_alumno.get(pregunta, "").strip()
126
+
127
+ if not resp_alumno_raw:
128
+ feedback.append(
129
  f"**{pregunta}**\n"
130
  f"Respuesta del alumno: No fue asignada.\n"
131
+ f"Respuesta correcta: {correct_clean}\n"
 
132
  )
133
+ # Se agrega al análisis, pero marcando que no fue asignada.
134
+ analisis.append({"pregunta": pregunta, "asignada": False})
135
  else:
136
+ alumno_clean = " ".join(resp_alumno_raw.split())
137
+ ratio = similar_textos(alumno_clean.lower(), correct_clean.lower())
138
+ if ratio >= 0.85:
139
+ eval_text = "La respuesta es correcta."
140
+ resultado = "correcta"
141
+ elif ratio >= 0.5:
142
+ eval_text = "La respuesta es incompleta. Se observa que faltan conceptos clave."
143
+ resultado = "incompleta"
144
  else:
145
+ eval_text = "La respuesta es incorrecta. No se refleja el mecanismo o concepto correcto."
146
+ resultado = "incorrecta"
147
+ feedback.append(
148
  f"**{pregunta}**\n"
149
+ f"Respuesta del alumno: {alumno_clean}\n"
150
+ f"Respuesta correcta: {correct_clean}\n"
151
+ f"{eval_text}\n"
152
  )
153
+ analisis.append({"pregunta": pregunta, "asignada": True, "resultado": resultado})
154
+ return "\n".join(feedback), analisis
155
 
156
  # -----------
157
+ # FUNCIÓN PRINCIPAL
158
  # -----------
159
  def revisar_examen(json_cred, pdf_docente, pdf_alumno):
160
  """
161
+ Función generadora que:
 
162
  1. Configura credenciales.
163
+ 2. Extrae y parsea el contenido de los PDFs.
164
+ 3. Compara las respuestas del alumno con las correctas.
165
+ 4. Genera una retroalimentación detallada por pregunta.
166
+ 5. Llama a un LLM para obtener un resumen final que incluya:
167
+ - Puntos fuertes (conceptos bien entendidos).
168
+ - Puntos a reforzar (respuestas incompletas o incorrectas).
169
+ - Recomendación general (solo considerando las preguntas asignadas).
170
  """
171
  yield "Cargando credenciales..."
172
  try:
173
  configurar_credenciales(json_cred.name)
174
+
175
  yield "Inicializando Vertex AI..."
176
  vertexai.init(project="deploygpt", location="us-central1")
177
+
178
  yield "Extrayendo texto del PDF del docente..."
179
  texto_docente = extraer_texto(pdf_docente.name)
180
+
181
  yield "Extrayendo texto del PDF del alumno..."
182
  texto_alumno = extraer_texto(pdf_alumno.name)
183
+
184
  yield "Dividiendo secciones (docente)..."
185
  preguntas_doc, respuestas_doc = split_secciones(texto_docente)
186
+
187
  yield "Dividiendo secciones (alumno)..."
188
  preguntas_alum, respuestas_alum = split_secciones(texto_alumno)
189
+
190
  yield "Parseando enumeraciones (docente)..."
191
  dict_preg_doc = parsear_enumeraciones(preguntas_doc)
192
  dict_resp_doc = parsear_enumeraciones(respuestas_doc)
193
+
194
+ # Unir las respuestas del docente (correctas)
195
  dict_docente = {}
196
+ for key in dict_preg_doc:
197
+ dict_docente[key] = dict_resp_doc.get(key, "")
198
+
 
199
  yield "Parseando enumeraciones (alumno)..."
200
  dict_preg_alum = parsear_enumeraciones(preguntas_alum)
201
  dict_resp_alum = parsear_enumeraciones(respuestas_alum)
202
+
203
+ # Unir las respuestas del alumno
204
  dict_alumno = {}
205
+ for key in dict_preg_alum:
206
+ dict_alumno[key] = dict_resp_alum.get(key, "")
207
+
 
208
  yield "Comparando preguntas y respuestas..."
209
+ feedback_text, analisis = comparar_preguntas_respuestas(dict_docente, dict_alumno)
210
+
211
+ if len(feedback_text.strip()) < 5:
212
  yield "No se encontraron preguntas o respuestas válidas."
213
  return
214
+
215
+ # Generar resumen global utilizando el LLM
216
+ # Se filtran solo las preguntas asignadas (se omiten las que no fueron asignadas)
217
+ analisis_asignadas = [a for a in analisis if a.get("asignada")]
218
+ resumen_prompt = f"""
219
+ A continuación se presenta el análisis por pregunta de un examen sobre la regulación del colesterol, considerando solo las preguntas asignadas al alumno:
220
+
221
+ {analisis_asignadas}
222
 
223
+ Con base en este análisis, genera un resumen del desempeño del alumno en el examen que incluya:
224
+ - Puntos fuertes: conceptos que el alumno ha comprendido correctamente.
225
+ - Puntos a reforzar: preguntas en las que la respuesta fue incompleta o incorrecta, indicando qué conceptos clave faltaron o se confundieron.
226
+ - Una recomendación general sobre si el alumno demuestra comprender los fundamentos o si necesita repasar el tema.
227
+
228
+ No incluyas en el análisis las preguntas que no fueron asignadas.
229
+ """
230
  yield "Generando resumen final con LLM..."
 
231
  model = GenerativeModel(
232
  "gemini-1.5-pro-001",
233
+ system_instruction=["Eres un profesor experto en bioquímica. Evalúa el desempeño del alumno basándote en los conceptos clave, sin inventar elementos adicionales."]
234
  )
235
+ summary_part = Part.from_text(resumen_prompt)
 
 
 
 
 
236
  summary_resp = model.generate_content(
237
  [summary_part],
238
  generation_config=generation_config,
239
  safety_settings=safety_settings,
240
  stream=False
241
  )
242
+ resumen_final = summary_resp.text.strip()
243
+
244
+ final_result = f"{feedback_text}\n\n**Resumen del desempeño:**\n{resumen_final}"
245
  yield final_result
246
+
247
  except Exception as e:
248
  yield f"Error al procesar: {str(e)}"
249
 
 
262
  description=(
263
  "Sube las credenciales, el PDF del docente (con las preguntas y respuestas correctas) y el PDF del alumno. "
264
  "El sistema separa las secciones 'Preguntas' y 'RESPUESTAS', parsea las enumeraciones y luego compara las respuestas. "
265
+ "Se evalúa si el alumno comprende los conceptos fundamentales: si la respuesta está incompleta se indica qué falta, "
266
+ "si es incorrecta se comenta por qué, y se omiten las preguntas no asignadas. Finalmente, se genera un resumen con recomendaciones."
267
  )
268
  )
269
 
270
  interface.launch(debug=True)
271
+