File size: 6,260 Bytes
00832a8
b54a6bc
 
19c3ca0
7cba34a
 
19c3ca0
 
7cba34a
f025ed5
d329e85
19c3ca0
7cba34a
f025ed5
7cba34a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b54a6bc
e341366
4d995a1
b54a6bc
 
 
 
 
 
 
 
b527142
19c3ca0
b527142
 
 
 
 
 
 
 
85b6c95
 
b527142
 
 
 
 
85b6c95
b527142
 
 
85b6c95
b527142
 
85b6c95
 
 
 
b527142
85b6c95
1b22788
19c3ca0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b22788
b527142
19c3ca0
 
 
 
b527142
19c3ca0
 
 
 
 
 
 
b54a6bc
 
f025ed5
 
 
 
 
 
7cba34a
b54a6bc
f025ed5
 
 
e341366
d35d511
f025ed5
 
19c3ca0
f025ed5
 
19c3ca0
 
f025ed5
 
1b22788
 
 
 
b527142
f025ed5
 
b527142
19c3ca0
f025ed5
 
19c3ca0
 
 
f025ed5
 
85b6c95
f025ed5
 
19c3ca0
85b6c95
19c3ca0
 
 
 
1b22788
19c3ca0
 
1e129b0
 
19c3ca0
1e129b0
f025ed5
7cba34a
f025ed5
7cba34a
f025ed5
 
19c3ca0
f025ed5
7cba34a
 
 
b54a6bc
b527142
b54a6bc
7cba34a
f025ed5
 
 
b527142
 
 
f025ed5
b527142
7cba34a
 
f61af4a
f025ed5
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
import gradio as gr
import PyPDF2
import os
import json
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):
    os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = json_path

def extraer_texto(pdf_path: str) -> str:
    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

def parsear_con_llm_con_enumeraciones(texto_pdf: str, model: GenerativeModel) -> dict:
    """
    Usa el LLM para extraer las 'Preguntas' y sus 'Respuestas'.
    Reconoce enumeraciones en 'Preguntas' (ej. '1.' o '1)') y en 'RESPUESTAS'.
    Devuelve JSON con la forma:
    {
      "Pregunta 1": "Texto de la respuesta",
      "Pregunta 2": "Texto de la respuesta",
      ...
    }
    """
    prompt = f"""
Eres un parser de texto que recibe el contenido de un PDF con dos secciones:
'Preguntas' y 'RESPUESTAS', cada una enumerada como '1.', '2)', etc.
Tu tarea es emparejar cada pregunta con su respuesta correspondiente, basándote
en el número que las identifica (1, 2, 3, ...).
Devuelve un JSON con la estructura:
{{
  "Pregunta 1": "texto de la respuesta",
  "Pregunta 2": "texto de la respuesta",
  ...
}}
Si no hay correspondencia entre pregunta y respuesta, la dejas vacía.
Ejemplo de JSON vacío: {{}}

Texto PDF:
{texto_pdf}

Devuelve solo el JSON, sin explicaciones adicionales.
"""
    part_text = Part.from_text(prompt)
    response = model.generate_content(
        [part_text],
        generation_config=generation_config,
        safety_settings=safety_settings,
        stream=False
    )
    try:
        data = json.loads(response.text.strip())
        if isinstance(data, dict):
            return data
        else:
            return {}
    except:
        return {}

def comparar_preguntas_respuestas(dict_docente: dict, dict_alumno: dict) -> str:
    """Compara dict_docente vs dict_alumno y retorna retroalimentación."""
    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)

def revisar_examen(json_cred, pdf_docente, pdf_alumno):
    """
    Función convertida en generador para mostrar 'progreso' en Gradio:
    en lugar de return final, se hace yield en varios pasos.
    """
    # Paso 1: Configurar credenciales
    yield "Cargando credenciales..."
    try:
        configurar_credenciales(json_cred.name)

        # Paso 2: Inicializar Vertex AI
        yield "Inicializando Vertex AI..."
        vertexai.init(project="deploygpt", location="us-central1")

        # Paso 3: Leer texto PDF
        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)

        # Paso 4: Invocar modelo
        yield "Parseando preguntas y respuestas (Docente)..."
        model = GenerativeModel(
            "gemini-1.5-pro-001",
            system_instruction=["Eres un parser estricto."]
        )
        dict_docente = parsear_con_llm_con_enumeraciones(texto_docente, model)

        yield "Parseando preguntas y respuestas (Alumno)..."
        dict_alumno = parsear_con_llm_con_enumeraciones(texto_alumno, model)

        # Paso 5: Comparar
        yield "Comparando respuestas..."
        feedback = comparar_preguntas_respuestas(dict_docente, dict_alumno)

        if len(feedback.strip()) < 5:
            yield "No se encontraron preguntas o respuestas válidas."
            return  # terminamos la función

        # Paso 6: Resumen final
        yield "Generando resumen final..."
        summary_prompt = f"""
        Eres un profesor experto de bioquímica. Te muestro la 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 Gradio
interface = gr.Interface(
    fn=revisar_examen,
    inputs=[
        gr.File(label="Credenciales JSON"),
        gr.File(label="PDF del Docente"),
        gr.File(label="PDF Alumno")
    ],
    # Cuando la función es un generador, definimos outputs="text" (o gr.Textbox()).
    outputs="text",
    title="Revisión de Exámenes con Enumeraciones (Progreso)",
    description=(
        "Sube tus credenciales, el PDF del docente y del alumno. El LLM "
        "detectará enumeraciones (1., 2), etc.) en 'Preguntas' y 'RESPUESTAS' "
        "y mostrará progreso a medida que avanza."
    )
)

interface.launch(debug=True)