Spaces:
Sleeping
Sleeping
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)) |