Adzacam
Fix: Alinear modelos SQLAlchemy con DDL real de Supabase
0d70e89
Raw
History Blame
6.82 kB
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))