DemoProfeIA / app.py
cesar's picture
Update app.py
a978321 verified
raw
history blame
9.07 kB
import gradio as gr
import PyPDF2
import os
import re
import vertexai
from vertexai.generative_models import GenerativeModel, Part, SafetySetting
# --------------------
# 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' (o 'PREGUNTAS') y 'RESPUESTAS' (o 'RESPUESTAS').
- Devuelve (texto_preguntas, texto_respuestas).
Si no las encuentra, devuelvo (texto, "") o similar.
"""
# Usamos re.IGNORECASE para ignorar mayúsculas/minúsculas
# Buscamos la posición de 'Preguntas' y 'RESPUESTAS' en el string
match_preg = re.search(r'(?i)preguntas', texto)
match_resp = re.search(r'(?i)respuestas', texto)
if not match_preg or not match_resp:
# Si no encontramos ambas, devolvemos algo por defecto
return (texto, "")
start_preg = match_preg.end() # donde termina la palabra 'Preguntas'
start_resp = match_resp.start()
# Sección de 'Preguntas' = texto entre 'Preguntas' y 'RESPUESTAS'
# Sección de 'RESPUESTAS' = texto desde 'RESPUESTAS' hasta el final
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 que contiene enumeraciones del tipo '1. ...', '2. ...', etc.,
separa cada número y su contenido.
Retorna un dict: {"Pregunta 1": "contenido", "Pregunta 2": "contenido", ...}.
"""
# Dividimos en "bloques" usando lookahead para no perder el delimitador.
# Ej: 1. ... \n 2. ... \n
# Regex: busca línea que inicie con dígitos y un punto (ej: 1.)
bloques = re.split(r'(?=^\d+\.\s)', texto, flags=re.MULTILINE)
resultado = {}
for bloque in bloques:
bloque_limpio = bloque.strip()
if not bloque_limpio:
continue
# Tomamos la primera línea para ver "1. " o "2. "
linea_principal = bloque_limpio.split("\n", 1)[0]
# Extraer el número
match_num = re.match(r'^(\d+)\.\s*(.*)', linea_principal)
if match_num:
numero = match_num.group(1)
# El resto del contenido es el bloque completo sin la línea principal
# o bien group(2) + la parte posterior
resto = ""
if "\n" in bloque_limpio:
resto = bloque_limpio.split("\n", 1)[1].strip()
else:
# No hay más líneas, sólo la principal
resto = match_num.group(2)
resultado[f"Pregunta {numero}"] = resto.strip()
return resultado
# ------------
# COMPARACIÓN
# ------------
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, => 'No fue asignada'.
- Si sí está => mostramos la respuesta del alumno y la supuesta 'correcta'.
"""
retroalimentacion = []
for pregunta, resp_correcta in dict_docente.items():
resp_alumno = dict_alumno.get(pregunta, None)
if resp_alumno is None:
retroalimentacion.append(f"**{pregunta}**\nNo fue asignada al alumno.\n")
else:
retroalimentacion.append(
f"**{pregunta}**\n"
f"Respuesta del alumno: {resp_alumno}\n"
f"Respuesta correcta: {resp_correcta}\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.
1. Configuramos credenciales
2. Extraemos texto de PDFs
3. Separamos secciones 'Preguntas' y 'RESPUESTAS' en docente y alumno
4. Parseamos enumeraciones
5. Comparamos
6. Llamamos a LLM para 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)
# Unimos dict_preg_doc y dict_resp_doc para crear un dict final
# Ej: "Pregunta 1" en dict_preg_doc con "Pregunta 1" en dict_resp_doc
# => dict_docente["Pregunta 1"] = "Respuesta 1..."
dict_docente = {}
for key_preg, texto_preg in dict_preg_doc.items():
# Revisar si en dict_resp_doc hay el mismo 'Pregunta X'
resp_doc = dict_resp_doc.get(key_preg, "")
# Unimos la respuesta en un sólo string
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 en un dict final de alumno
dict_alumno = {}
for key_preg, texto_preg in dict_preg_alum.items():
resp_alum = dict_resp_alum.get(key_preg, "")
dict_alumno[key_preg] = resp_alum
yield "Comparando preguntas/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
sin inventar preguntas adicionales.
"""
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
# -----------------
import gradio as gr
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", # so we can see partial yields
title="Revisión de Exámenes (Preguntas/Respuestas enumeradas)",
description=(
"Sube credenciales, el PDF del docente y del alumno. "
"Se busca la palabra 'Preguntas' y 'RESPUESTAS', parseamos enumeraciones (1., 2., etc.), "
"luego comparamos y finalmente pedimos un resumen al LLM."
)
)
interface.launch(debug=True)