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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -54
app.py CHANGED
@@ -4,6 +4,7 @@ import os
4
  import re
5
  import vertexai
6
  from vertexai.generative_models import GenerativeModel, Part, SafetySetting
 
7
 
8
  # --------------------
9
  # CONFIGURACIÓN GLOBAL
@@ -57,81 +58,82 @@ def extraer_texto(pdf_path: str) -> str:
57
  def split_secciones(texto: str) -> (str, str):
58
  """
59
  Separa el texto en dos partes: la sección 'Preguntas' y la sección 'RESPUESTAS'.
60
- - Busca la palabra 'Preguntas' (o 'PREGUNTAS') y 'RESPUESTAS' (o 'RESPUESTAS').
61
- - Devuelve (texto_preguntas, texto_respuestas).
62
- Si no las encuentra, devuelvo (texto, "") o similar.
63
  """
64
- # Usamos re.IGNORECASE para ignorar mayúsculas/minúsculas
65
- # Buscamos la posición de 'Preguntas' y 'RESPUESTAS' en el string
66
  match_preg = re.search(r'(?i)preguntas', texto)
67
  match_resp = re.search(r'(?i)respuestas', texto)
68
 
69
  if not match_preg or not match_resp:
70
- # Si no encontramos ambas, devolvemos algo por defecto
71
  return (texto, "")
72
-
73
- start_preg = match_preg.end() # donde termina la palabra 'Preguntas'
74
  start_resp = match_resp.start()
75
 
76
- # Sección de 'Preguntas' = texto entre 'Preguntas' y 'RESPUESTAS'
77
- # Sección de 'RESPUESTAS' = texto desde 'RESPUESTAS' hasta el final
78
  texto_preguntas = texto[start_preg:start_resp].strip()
79
  texto_respuestas = texto[match_resp.end():].strip()
80
  return (texto_preguntas, texto_respuestas)
81
 
82
  def parsear_enumeraciones(texto: str) -> dict:
83
  """
84
- Dado un texto que contiene enumeraciones del tipo '1. ...', '2. ...', etc.,
85
  separa cada número y su contenido.
86
  Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
87
  """
88
- # Dividimos en "bloques" usando lookahead para no perder el delimitador.
89
- # Ej: 1. ... \n 2. ... \n
90
- # Regex: busca línea que inicie con dígitos y un punto (ej: 1.)
91
  bloques = re.split(r'(?=^\d+\.\s)', texto, flags=re.MULTILINE)
92
-
93
  resultado = {}
94
  for bloque in bloques:
95
  bloque_limpio = bloque.strip()
96
  if not bloque_limpio:
97
  continue
98
- # Tomamos la primera línea para ver "1. " o "2. "
99
  linea_principal = bloque_limpio.split("\n", 1)[0]
100
- # Extraer el número
101
  match_num = re.match(r'^(\d+)\.\s*(.*)', linea_principal)
102
  if match_num:
103
  numero = match_num.group(1)
104
- # El resto del contenido es el bloque completo sin la línea principal
105
- # o bien group(2) + la parte posterior
106
- resto = ""
107
  if "\n" in bloque_limpio:
108
  resto = bloque_limpio.split("\n", 1)[1].strip()
109
  else:
110
- # No hay más líneas, sólo la principal
111
  resto = match_num.group(2)
112
-
113
  resultado[f"Pregunta {numero}"] = resto.strip()
114
  return resultado
115
 
116
  # ------------
117
  # COMPARACIÓN
118
  # ------------
 
 
 
 
119
  def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> str:
120
  """
121
  Compara dict_docente vs dict_alumno y retorna retroalimentación.
122
- - Si la 'Pregunta X' no está en dict_alumno, => 'No fue asignada'.
123
- - Si está => mostramos la respuesta del alumno y la supuesta 'correcta'.
 
124
  """
125
  retroalimentacion = []
126
  for pregunta, resp_correcta in dict_docente.items():
127
  resp_alumno = dict_alumno.get(pregunta, None)
128
- if resp_alumno is None:
129
- retroalimentacion.append(f"**{pregunta}**\nNo fue asignada al alumno.\n")
 
 
 
 
 
130
  else:
 
 
 
 
 
 
 
 
131
  retroalimentacion.append(
132
  f"**{pregunta}**\n"
133
- f"Respuesta del alumno: {resp_alumno}\n"
134
- f"Respuesta correcta: {resp_correcta}\n"
 
135
  )
136
  return "\n".join(retroalimentacion)
137
 
@@ -141,12 +143,13 @@ def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> str:
141
  def revisar_examen(json_cred, pdf_docente, pdf_alumno):
142
  """
143
  Función generadora que muestra progreso en Gradio con yield.
144
- 1. Configuramos credenciales
145
- 2. Extraemos texto de PDFs
146
- 3. Separamos secciones 'Preguntas' y 'RESPUESTAS' en docente y alumno
147
- 4. Parseamos enumeraciones
148
- 5. Comparamos
149
- 6. Llamamos a LLM para un resumen final
 
150
  """
151
  yield "Cargando credenciales..."
152
  try:
@@ -171,27 +174,23 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
171
  dict_preg_doc = parsear_enumeraciones(preguntas_doc)
172
  dict_resp_doc = parsear_enumeraciones(respuestas_doc)
173
 
174
- # Unimos dict_preg_doc y dict_resp_doc para crear un dict final
175
- # Ej: "Pregunta 1" en dict_preg_doc con "Pregunta 1" en dict_resp_doc
176
- # => dict_docente["Pregunta 1"] = "Respuesta 1..."
177
  dict_docente = {}
178
- for key_preg, texto_preg in dict_preg_doc.items():
179
- # Revisar si en dict_resp_doc hay el mismo 'Pregunta X'
180
  resp_doc = dict_resp_doc.get(key_preg, "")
181
- # Unimos la respuesta en un sólo string
182
  dict_docente[key_preg] = resp_doc
183
 
184
  yield "Parseando enumeraciones (alumno)..."
185
  dict_preg_alum = parsear_enumeraciones(preguntas_alum)
186
  dict_resp_alum = parsear_enumeraciones(respuestas_alum)
187
 
188
- # Unir en un dict final de alumno
189
  dict_alumno = {}
190
- for key_preg, texto_preg in dict_preg_alum.items():
191
  resp_alum = dict_resp_alum.get(key_preg, "")
192
  dict_alumno[key_preg] = resp_alum
193
 
194
- yield "Comparando preguntas/respuestas..."
195
  feedback = comparar_preguntas_respuestas(dict_docente, dict_alumno)
196
 
197
  if len(feedback.strip()) < 5:
@@ -205,11 +204,10 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
205
  system_instruction=["Eres un profesor experto de bioquímica. No inventes preguntas."]
206
  )
207
  summary_prompt = f"""
208
- Comparación de preguntas y respuestas:
209
- {feedback}
210
- Por favor, genera un breve resumen del desempeño del alumno
211
- sin inventar preguntas adicionales.
212
- """
213
  summary_part = Part.from_text(summary_prompt)
214
  summary_resp = model.generate_content(
215
  [summary_part],
@@ -227,8 +225,6 @@ def revisar_examen(json_cred, pdf_docente, pdf_alumno):
227
  # -----------------
228
  # INTERFAZ DE GRADIO
229
  # -----------------
230
- import gradio as gr
231
-
232
  interface = gr.Interface(
233
  fn=revisar_examen,
234
  inputs=[
@@ -236,12 +232,12 @@ interface = gr.Interface(
236
  gr.File(label="PDF del Docente"),
237
  gr.File(label="PDF del Alumno")
238
  ],
239
- outputs="text", # so we can see partial yields
240
  title="Revisión de Exámenes (Preguntas/Respuestas enumeradas)",
241
  description=(
242
- "Sube credenciales, el PDF del docente y del alumno. "
243
- "Se busca la palabra 'Preguntas' y 'RESPUESTAS', parseamos enumeraciones (1., 2., etc.), "
244
- "luego comparamos y finalmente pedimos un resumen al LLM."
245
  )
246
  )
247
 
 
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
 
58
  def split_secciones(texto: str) -> (str, str):
59
  """
60
  Separa el texto en dos partes: la sección 'Preguntas' y la sección 'RESPUESTAS'.
61
+ Busca la palabra 'Preguntas' y 'RESPUESTAS' (ignorando mayúsculas/minúsculas).
 
 
62
  """
 
 
63
  match_preg = re.search(r'(?i)preguntas', texto)
64
  match_resp = re.search(r'(?i)respuestas', texto)
65
 
66
  if not match_preg or not match_resp:
 
67
  return (texto, "")
68
+
69
+ start_preg = match_preg.end() # fin de la palabra 'Preguntas'
70
  start_resp = match_resp.start()
71
 
 
 
72
  texto_preguntas = texto[start_preg:start_resp].strip()
73
  texto_respuestas = texto[match_resp.end():].strip()
74
  return (texto_preguntas, texto_respuestas)
75
 
76
  def parsear_enumeraciones(texto: str) -> dict:
77
  """
78
+ Dado un texto con enumeraciones del tipo '1. ...', '2. ...', etc.,
79
  separa cada número y su contenido.
80
  Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
81
  """
 
 
 
82
  bloques = re.split(r'(?=^\d+\.\s)', texto, flags=re.MULTILINE)
 
83
  resultado = {}
84
  for bloque in bloques:
85
  bloque_limpio = bloque.strip()
86
  if not bloque_limpio:
87
  continue
 
88
  linea_principal = bloque_limpio.split("\n", 1)[0]
 
89
  match_num = re.match(r'^(\d+)\.\s*(.*)', linea_principal)
90
  if match_num:
91
  numero = match_num.group(1)
 
 
 
92
  if "\n" in bloque_limpio:
93
  resto = bloque_limpio.split("\n", 1)[1].strip()
94
  else:
 
95
  resto = match_num.group(2)
 
96
  resultado[f"Pregunta {numero}"] = resto.strip()
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
 
 
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:
 
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:
 
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],
 
225
  # -----------------
226
  # INTERFAZ DE GRADIO
227
  # -----------------
 
 
228
  interface = gr.Interface(
229
  fn=revisar_examen,
230
  inputs=[
 
232
  gr.File(label="PDF del Docente"),
233
  gr.File(label="PDF del Alumno")
234
  ],
235
+ outputs="text",
236
  title="Revisión de Exámenes (Preguntas/Respuestas enumeradas)",
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