File size: 6,818 Bytes
d923814
 
 
 
 
0d70e89
 
 
 
 
 
 
 
 
 
d923814
 
 
 
 
 
 
 
 
 
 
0d70e89
 
 
 
d923814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d70e89
 
f245e96
 
 
d923814
0d70e89
f245e96
0d70e89
 
 
f245e96
 
 
0d70e89
d923814
0d70e89
f245e96
0d70e89
 
 
 
 
 
 
f245e96
0d70e89
 
 
 
 
 
 
 
f245e96
0d70e89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8fe5c0
0d70e89
 
 
 
 
 
 
 
 
 
 
 
f245e96
 
 
 
 
d923814
0d70e89
d923814
 
 
 
 
 
 
 
 
 
 
 
 
 
0d70e89
 
d923814
 
e43f94c
d923814
 
 
 
 
0d70e89
e43f94c
f245e96
11036a4
0d70e89
11036a4
 
 
 
 
 
 
0d70e89
11036a4
 
 
 
 
 
0d70e89
 
11036a4
 
0d70e89
 
11036a4
 
 
 
0d70e89
11036a4
 
e43f94c
0d70e89
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
import datetime
import logging
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from database import (
    get_db,
    DimEstudiante,
    DimDocente,
    DimModulo,
    DimTiempo,
    DimOrigenDocumental,
    Users,
    FactRendimientoAcademico,
)
from ner_engine import ner_engine

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(
    title="GiraGroup BI Backend Cloud",
    description="API para Tecnologías Emergentes II con BETO y Supabase",
    version="1.0.0"
)

# El nombre del comment en el esquema DDL de Supabase indica que el CHECK de
# tipo_documento es: ('SHEET', 'FORM', 'MOODLE', 'XLSX')
TIPO_DOC_VALIDO = "SHEET"

class ProcessSheetPayload(BaseModel):
    texto_celda: str
    nota_detectada: float
    asistencia: float
    incumplimiento_tareas: float
    id_docente: int
    id_modulo: int
    id_tiempo: int
    id_documento: int
    id_usuario: int

@app.get("/")
def read_root():
    return {
        "status": "healthy",
        "service": "GiraGroup BI Backend API Cloud",
        "ner_initialized": ner_engine._initialized or ner_engine.pipeline is not None
    }

@app.post("/api/v1/ingesta/tabular", status_code=status.HTTP_201_CREATED)
def procesar_registro_tabular(payload: ProcessSheetPayload, db: Session = Depends(get_db)):
    try:
        # 1. NLP con BETO
        entidades = ner_engine.extract_entities(payload.texto_celda)
        confianza_ia = sum([e["score"] for e in entidades]) / len(entidades) if entidades else 1.0
        forzar_revision = confianza_ia < 0.60

        # 2. Dimensión Estudiante
        nombre_resuelto = payload.texto_celda.strip()
        estudiante = db.query(DimEstudiante).filter(
            DimEstudiante.nombre_completo == nombre_resuelto
        ).first()
        if not estudiante:
            estudiante = DimEstudiante(nombre_completo=nombre_resuelto)
            db.add(estudiante)
            db.flush()

        # 3. Dimensión Docente — columnas reales: nombre_completo, area_especialidad
        if not db.query(DimDocente).filter(DimDocente.id_docente == payload.id_docente).first():
            db.add(DimDocente(
                id_docente=payload.id_docente,
                nombre_completo="Docente Generico",
                area_especialidad="Generico"
            ))

        # 4. Dimensión Módulo — columnas reales: nombre_modulo, nombre_institucion, programa
        if not db.query(DimModulo).filter(DimModulo.id_modulo == payload.id_modulo).first():
            db.add(DimModulo(
                id_modulo=payload.id_modulo,
                nombre_modulo="Modulo Generico",
                nombre_institucion="GiraGroup",
                programa="General"
            ))

        # 5. Dimensión Tiempo — columnas reales: gestion, semestre, mes
        if not db.query(DimTiempo).filter(DimTiempo.id_tiempo == payload.id_tiempo).first():
            db.add(DimTiempo(
                id_tiempo=payload.id_tiempo,
                gestion=2026,
                semestre=1,
                mes="Mayo"
            ))

        # 6. Dimensión Origen Documental — tabla real: dim_origen_documental
        #    CHECK: tipo_documento IN ('SHEET', 'FORM', 'MOODLE', 'XLSX')
        if not db.query(DimOrigenDocumental).filter(
            DimOrigenDocumental.id_documento == payload.id_documento
        ).first():
            db.add(DimOrigenDocumental(
                id_documento=payload.id_documento,
                tipo_documento=TIPO_DOC_VALIDO,
                nombre_archivo="carga_automatica"
            ))

        # 7. Usuario — tabla real: users (id, username, hashed_password, role)
        if not db.query(Users).filter(Users.id == payload.id_usuario).first():
            db.add(Users(
                id=payload.id_usuario,
                username=f"sistema_{payload.id_usuario}",
                hashed_password="$placeholder$",
                role="admin"
            ))

        db.flush()

        # 8. Alertas estratégicas
        alertas_disparadas = []
        if payload.nota_detectada <= 70.0:
            alertas_disparadas.append("RIESGO_ACADEMICO_CRITICO")
        if payload.asistencia < 70.0 or payload.incumplimiento_tareas > 30.0:
            alertas_disparadas.append("RIESGO_DESERCION_ALTA")

        # 9. Insertar hecho con las FK correctas
        nuevo_hecho = FactRendimientoAcademico(
            id_estudiante=estudiante.id_estudiante,
            id_docente=payload.id_docente,
            id_modulo=payload.id_modulo,
            id_tiempo=payload.id_tiempo,
            id_documento=payload.id_documento,
            id_usuario_carga=payload.id_usuario,
            nota_final=payload.nota_detectada,
            asistencia_pct=payload.asistencia,
            incumplimiento_actividades_pct=payload.incumplimiento_tareas,
            nivel_confianza_ia=confianza_ia,
            requiere_revision=forzar_revision
        )
        db.add(nuevo_hecho)
        db.commit()

        return {
            "status": "processed",
            "id_estudiante_asignado": estudiante.id_estudiante,
            "confianza_modelo_beto": round(confianza_ia, 4),
            "requiere_auditoria_humana": forzar_revision,
            "alertas_estrategicas": alertas_disparadas
        }
    except Exception as err:
        db.rollback()
        logger.error(f"Error crítico en backend 500: {err}")
        raise HTTPException(status_code=500, detail=str(err))


@app.get("/api/v1/riesgos/cruzado")
def obtener_riesgos_cruzados(limite_nota: float = 70.0, min_cuotas: int = 2, db: Session = Depends(get_db)):
    try:
        resultados = db.query(DimEstudiante, FactRendimientoAcademico).\
            join(FactRendimientoAcademico, DimEstudiante.id_estudiante == FactRendimientoAcademico.id_estudiante).\
            filter(FactRendimientoAcademico.nota_final <= limite_nota).\
            all()

        data = []
        for est, fact in resultados:
            data.append({
                "estudiante": est.nombre_completo,
                "codigo": f"EST-{est.id_estudiante:06d}",
                "rendimiento": {
                    "nota_actual": float(fact.nota_final),
                    "estado_academico": "CRÍTICO" if fact.nota_final <= 70 else "REGULAR"
                },
                "finanzas": {
                    "cuotas_mora": min_cuotas,
                    "deuda_total": 350.0 * min_cuotas,
                    "estado_cartera": "MORA"
                },
                "nivel_riesgo_global": "ALTO - CRÍTICO"
            })

        return {"status": "success", "data": data}
    except Exception as e:
        logger.error(f"Fallo en la resolución del query OLAP: {e}")
        raise HTTPException(status_code=500, detail=str(e))