DemoProfeIA / app.py
cesar's picture
Update app.py
b7193be verified
raw
history blame
9.11 kB
import gradio as gr
import PyPDF2
import os
import re
import vertexai
from vertexai.generative_models import GenerativeModel, Part, SafetySetting
from difflib import SequenceMatcher # Para comparar similitud
# --------------------
# CONFIGURACIÓN GLOBAL
# --------------------
generation_config = {
"max_output_tokens": 8192,
"temperature": 0,
"top_p": 0.8,
}
safety_settings = [
SafetySetting(
category=SafetySetting.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
threshold=SafetySetting.HarmBlockThreshold.OFF
),
SafetySetting(
category=SafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=SafetySetting.HarmBlockThreshold.OFF
),
SafetySetting(
category=SafetySetting.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold=SafetySetting.HarmBlockThreshold.OFF
),
SafetySetting(
category=SafetySetting.HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold=SafetySetting.HarmBlockThreshold.OFF
),
]
def configurar_credenciales(json_path: str):
"""Configura credenciales de Google Cloud a partir de un archivo JSON."""
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = json_path
# -----------
# LECTURA PDF
# -----------
def extraer_texto(pdf_path: str) -> str:
"""
Extrae el texto de todas las páginas de un PDF con PyPDF2.
Retorna un string con todo el texto concatenado.
"""
texto_total = ""
with open(pdf_path, "rb") as f:
lector = PyPDF2.PdfReader(f)
for page in lector.pages:
texto_total += page.extract_text() or ""
return texto_total
# -----------
# PARSEO TEXTO
# -----------
def split_secciones(texto: str) -> (str, str):
"""
Separa el texto en dos partes: la sección 'Preguntas' y la sección 'RESPUESTAS'.
Busca la palabra 'Preguntas' y 'RESPUESTAS' (ignorando mayúsculas/minúsculas).
"""
match_preg = re.search(r'(?i)preguntas', texto)
match_resp = re.search(r'(?i)respuestas', texto)
if not match_preg or not match_resp:
return (texto, "")
start_preg = match_preg.end() # fin de la palabra 'Preguntas'
start_resp = match_resp.start()
texto_preguntas = texto[start_preg:start_resp].strip()
texto_respuestas = texto[match_resp.end():].strip()
return (texto_preguntas, texto_respuestas)
def parsear_enumeraciones(texto: str) -> dict:
"""
Dado un texto con enumeraciones del tipo '1. ...', '2. ...', etc.,
separa cada número y su contenido.
Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
"""
bloques = re.split(r'(?=^\d+\.\s)', texto, flags=re.MULTILINE)
resultado = {}
for bloque in bloques:
bloque_limpio = bloque.strip()
if not bloque_limpio:
continue
linea_principal = bloque_limpio.split("\n", 1)[0]
match_num = re.match(r'^(\d+)\.\s*(.*)', linea_principal)
if match_num:
numero = match_num.group(1)
if "\n" in bloque_limpio:
resto = bloque_limpio.split("\n", 1)[1].strip()
else:
resto = match_num.group(2)
resultado[f"Pregunta {numero}"] = resto.strip()
return resultado
# ------------
# COMPARACIÓN
# ------------
def similar_textos(texto1: str, texto2: str) -> float:
"""Calcula la similitud entre dos textos (valor entre 0 y 1)."""
return SequenceMatcher(None, texto1, texto2).ratio()
def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> str:
"""
Compara dict_docente vs dict_alumno y retorna retroalimentación.
- Si la 'Pregunta X' no está en dict_alumno, se recomienda revisar el tema.
- Si está, se compara la respuesta del alumno con la correcta.
Se eliminan los saltos de línea en la respuesta del alumno.
"""
retroalimentacion = []
for pregunta, resp_correcta in dict_docente.items():
resp_alumno = dict_alumno.get(pregunta, None)
if resp_alumno is None or resp_alumno.strip() == "":
retroalimentacion.append(
f"**{pregunta}**\n"
f"Respuesta del alumno: No fue asignada.\n"
f"Respuesta correcta: {' '.join(resp_correcta.split())}\n"
f"Recomendación: Revisar el tema correspondiente.\n"
)
else:
# Eliminar saltos de línea y espacios extra
resp_alumno_clean = " ".join(resp_alumno.split())
resp_correcta_clean = " ".join(resp_correcta.split())
ratio = similar_textos(resp_alumno_clean.lower(), resp_correcta_clean.lower())
if ratio >= 0.8:
feedback_text = "La respuesta es correcta."
else:
feedback_text = "La respuesta no coincide completamente. Se recomienda revisar la explicación y reforzar el concepto."
retroalimentacion.append(
f"**{pregunta}**\n"
f"Respuesta del alumno: {resp_alumno_clean}\n"
f"Respuesta correcta: {resp_correcta_clean}\n"
f"{feedback_text}\n"
)
return "\n".join(retroalimentacion)
# -----------
# FUNCIÓN LÓGICA
# -----------
def revisar_examen(json_cred, pdf_docente, pdf_alumno):
"""
Función generadora que muestra progreso en Gradio con yield.
Realiza los siguientes pasos:
1. Configura credenciales.
2. Extrae texto de los PDFs.
3. Separa secciones 'Preguntas' y 'RESPUESTAS'.
4. Parsea las enumeraciones.
5. Compara las respuestas y genera retroalimentación con recomendaciones.
6. Llama a un LLM para generar un resumen final.
"""
yield "Cargando credenciales..."
try:
configurar_credenciales(json_cred.name)
yield "Inicializando Vertex AI..."
vertexai.init(project="deploygpt", location="us-central1")
yield "Extrayendo texto del PDF del docente..."
texto_docente = extraer_texto(pdf_docente.name)
yield "Extrayendo texto del PDF del alumno..."
texto_alumno = extraer_texto(pdf_alumno.name)
yield "Dividiendo secciones (docente)..."
preguntas_doc, respuestas_doc = split_secciones(texto_docente)
yield "Dividiendo secciones (alumno)..."
preguntas_alum, respuestas_alum = split_secciones(texto_alumno)
yield "Parseando enumeraciones (docente)..."
dict_preg_doc = parsear_enumeraciones(preguntas_doc)
dict_resp_doc = parsear_enumeraciones(respuestas_doc)
# Unir preguntas y respuestas del docente
dict_docente = {}
for key_preg in dict_preg_doc:
resp_doc = dict_resp_doc.get(key_preg, "")
dict_docente[key_preg] = resp_doc
yield "Parseando enumeraciones (alumno)..."
dict_preg_alum = parsear_enumeraciones(preguntas_alum)
dict_resp_alum = parsear_enumeraciones(respuestas_alum)
# Unir preguntas y respuestas del alumno
dict_alumno = {}
for key_preg in dict_preg_alum:
resp_alum = dict_resp_alum.get(key_preg, "")
dict_alumno[key_preg] = resp_alum
yield "Comparando preguntas y respuestas..."
feedback = comparar_preguntas_respuestas(dict_docente, dict_alumno)
if len(feedback.strip()) < 5:
yield "No se encontraron preguntas o respuestas válidas."
return
yield "Generando resumen final con LLM..."
# Llamada final al LLM:
model = GenerativeModel(
"gemini-1.5-pro-001",
system_instruction=["Eres un profesor experto de bioquímica. No inventes preguntas."]
)
summary_prompt = f"""
Comparación de preguntas y respuestas:
{feedback}
Por favor, genera un breve resumen del desempeño del alumno, indicando si entiende los conceptos y recomendando reforzar los puntos necesarios.
"""
summary_part = Part.from_text(summary_prompt)
summary_resp = model.generate_content(
[summary_part],
generation_config=generation_config,
safety_settings=safety_settings,
stream=False
)
final_result = f"{feedback}\n\n**Resumen**\n{summary_resp.text.strip()}"
yield final_result
except Exception as e:
yield f"Error al procesar: {str(e)}"
# -----------------
# INTERFAZ DE GRADIO
# -----------------
interface = gr.Interface(
fn=revisar_examen,
inputs=[
gr.File(label="Credenciales JSON"),
gr.File(label="PDF del Docente"),
gr.File(label="PDF del Alumno")
],
outputs="text",
title="Revisión de Exámenes (Preguntas/Respuestas enumeradas)",
description=(
"Sube las credenciales, el PDF del docente (con las preguntas y respuestas correctas) y el PDF del alumno. "
"El sistema separa las secciones 'Preguntas' y 'RESPUESTAS', parsea las enumeraciones y luego compara las respuestas. "
"Finalmente, se genera un resumen con recomendaciones para reforzar los conceptos según el desempeño del alumno."
)
)
interface.launch(debug=True)