File size: 9,067 Bytes
00832a8
b54a6bc
 
a978321
7cba34a
 
19c3ca0
a978321
 
 
7cba34a
f025ed5
d329e85
19c3ca0
7cba34a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b54a6bc
a978321
e341366
4d995a1
a978321
 
 
b54a6bc
a978321
 
 
 
b54a6bc
 
 
 
 
 
 
a978321
 
 
 
19c3ca0
a978321
 
 
 
85b6c95
a978321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b22788
a978321
 
 
 
 
19c3ca0
 
 
 
b527142
19c3ca0
 
 
 
 
 
 
b54a6bc
a978321
 
 
b54a6bc
a978321
 
 
 
 
 
 
 
 
f025ed5
7cba34a
b54a6bc
a978321
f025ed5
e341366
d35d511
f025ed5
19c3ca0
f025ed5
 
19c3ca0
 
a978321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19c3ca0
 
 
f025ed5
63918e4
85b6c95
a978321
 
 
 
 
 
19c3ca0
a978321
19c3ca0
 
 
 
1b22788
19c3ca0
 
1e129b0
 
19c3ca0
1e129b0
63918e4
a978321
f025ed5
7cba34a
f025ed5
 
19c3ca0
a978321
 
 
63918e4
 
7cba34a
 
 
b54a6bc
b527142
a978321
7cba34a
a978321
 
b527142
a978321
 
 
b527142
7cba34a
 
f61af4a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
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)