Adzacam commited on
Commit
e43f94c
·
1 Parent(s): f245e96

Feat: Endpoint OLAP para cruce de riesgos académicos

Browse files
Files changed (1) hide show
  1. app.py +23 -16
app.py CHANGED
@@ -37,31 +37,35 @@ def read_root():
37
 
38
  @app.post("/api/v1/ingesta/tabular", status_code=status.HTTP_201_CREATED)
39
  def procesar_registro_tabular(payload: ProcessSheetPayload, db: Session = Depends(get_db)):
40
- try: # <--- TRY GLOBAL PARA EVITAR EL CRASH 500
41
  entidades = ner_engine.extract_entities(payload.texto_celda)
42
  confianza_ia = sum([e["score"] for e in entidades]) / len(entidades) if entidades else 1.0
43
  forzar_revision = confianza_ia < 0.60
44
 
45
  nombre_resuelto = payload.texto_celda.strip()
46
  estudiante = db.query(DimEstudiante).filter(DimEstudiante.nombre_completo == nombre_resuelto).first()
 
47
  if not estudiante:
48
  estudiante = DimEstudiante(nombre_completo=nombre_resuelto)
49
  db.add(estudiante)
50
- db.flush() # Flush en lugar de commit para mantener la transacción viva
51
 
52
- # Control de Dimensiones
53
  if not db.query(DimDocente).filter(DimDocente.id_docente == payload.id_docente).first():
54
  db.add(DimDocente(id_docente=payload.id_docente))
 
55
  if not db.query(DimModulo).filter(DimModulo.id_modulo == payload.id_modulo).first():
56
  db.add(DimModulo(id_modulo=payload.id_modulo))
 
57
  if not db.query(DimTiempo).filter(DimTiempo.id_tiempo == payload.id_tiempo).first():
58
  db.add(DimTiempo(id_tiempo=payload.id_tiempo))
 
59
  if not db.query(DimDocumento).filter(DimDocumento.id_documento == payload.id_documento).first():
60
  db.add(DimDocumento(id_documento=payload.id_documento))
 
61
  if not db.query(DimUsuario).filter(DimUsuario.id_usuario == payload.id_usuario).first():
62
  db.add(DimUsuario(id_usuario=payload.id_usuario))
63
-
64
- db.flush()
65
 
66
  alertas_disparadas = []
67
  if payload.nota_detectada <= 70.0:
@@ -83,25 +87,27 @@ def procesar_registro_tabular(payload: ProcessSheetPayload, db: Session = Depend
83
  requiere_revision=forzar_revision
84
  )
85
  db.add(nuevo_hecho)
86
- db.commit() # Un solo commit al final si TODO sale bien
 
 
87
 
88
  return {
89
  "status": "processed",
90
- "id_estudiante_assignado": estudiante.id_estudiante, # Wait, the user wrote id_estudiante_asignado in their snippet, let's keep it as id_estudiante_asignado
91
  "confianza_modelo_beto": round(confianza_ia, 4),
92
  "requiere_auditoria_humana": forzar_revision,
93
  "alertas_estrategicas": alertas_disparadas
94
  }
95
  except Exception as err:
96
- db.rollback()
97
- logger.error(f"Fallo crítico en pipeline: {err}")
 
98
  raise HTTPException(status_code=500, detail=str(err))
99
 
100
  @app.get("/api/v1/riesgos/cruzado")
101
-
102
  def obtener_riesgos_cruzados(limite_nota: float = 70.0, min_cuotas: int = 2, db: Session = Depends(get_db)):
103
  try:
104
- # Consulta transaccional al esquema estrella
105
  resultados = db.query(DimEstudiante, FactRendimientoAcademico).\
106
  join(FactRendimientoAcademico, DimEstudiante.id_estudiante == FactRendimientoAcademico.id_estudiante).\
107
  filter(FactRendimientoAcademico.nota_final <= limite_nota).\
@@ -109,16 +115,17 @@ def obtener_riesgos_cruzados(limite_nota: float = 70.0, min_cuotas: int = 2, db:
109
 
110
  data = []
111
  for est, fact in resultados:
 
112
  data.append({
113
  "estudiante": est.nombre_completo,
114
  "codigo": f"EST-{est.id_estudiante:06d}",
115
  "rendimiento": {
116
  "nota_actual": fact.nota_final,
117
- "estado_academico": "CRÍTICO" if fact.nota_final <= 70 else "REGULAR"
118
  },
119
  "finanzas": {
120
- "cuotas_mora": min_cuotas, # Dato dinámico a cruzar con fact_situacion_financiera posteriormente
121
- "deuda_total": 350.0 * min_cuotas,
122
  "estado_cartera": "MORA"
123
  },
124
  "nivel_riesgo_global": "ALTO - CRÍTICO"
@@ -126,5 +133,5 @@ def obtener_riesgos_cruzados(limite_nota: float = 70.0, min_cuotas: int = 2, db:
126
 
127
  return {"status": "success", "data": data}
128
  except Exception as e:
129
- logger.error(f"Error consultando riesgos: {e}")
130
- raise HTTPException(status_code=500, detail="Error de base de datos")
 
37
 
38
  @app.post("/api/v1/ingesta/tabular", status_code=status.HTTP_201_CREATED)
39
  def procesar_registro_tabular(payload: ProcessSheetPayload, db: Session = Depends(get_db)):
40
+ try: # Todo envuelto en un try-except
41
  entidades = ner_engine.extract_entities(payload.texto_celda)
42
  confianza_ia = sum([e["score"] for e in entidades]) / len(entidades) if entidades else 1.0
43
  forzar_revision = confianza_ia < 0.60
44
 
45
  nombre_resuelto = payload.texto_celda.strip()
46
  estudiante = db.query(DimEstudiante).filter(DimEstudiante.nombre_completo == nombre_resuelto).first()
47
+
48
  if not estudiante:
49
  estudiante = DimEstudiante(nombre_completo=nombre_resuelto)
50
  db.add(estudiante)
51
+ db.flush() # Importante: Usar flush para obtener el ID sin confirmar transacción final
52
 
 
53
  if not db.query(DimDocente).filter(DimDocente.id_docente == payload.id_docente).first():
54
  db.add(DimDocente(id_docente=payload.id_docente))
55
+
56
  if not db.query(DimModulo).filter(DimModulo.id_modulo == payload.id_modulo).first():
57
  db.add(DimModulo(id_modulo=payload.id_modulo))
58
+
59
  if not db.query(DimTiempo).filter(DimTiempo.id_tiempo == payload.id_tiempo).first():
60
  db.add(DimTiempo(id_tiempo=payload.id_tiempo))
61
+
62
  if not db.query(DimDocumento).filter(DimDocumento.id_documento == payload.id_documento).first():
63
  db.add(DimDocumento(id_documento=payload.id_documento))
64
+
65
  if not db.query(DimUsuario).filter(DimUsuario.id_usuario == payload.id_usuario).first():
66
  db.add(DimUsuario(id_usuario=payload.id_usuario))
67
+
68
+ db.flush() # Confirmamos la creación de las dimensiones
69
 
70
  alertas_disparadas = []
71
  if payload.nota_detectada <= 70.0:
 
87
  requiere_revision=forzar_revision
88
  )
89
  db.add(nuevo_hecho)
90
+
91
+ # Una vez que todo está correcto, hacemos el commit final
92
+ db.commit()
93
 
94
  return {
95
  "status": "processed",
96
+ "id_estudiante_asignado": estudiante.id_estudiante,
97
  "confianza_modelo_beto": round(confianza_ia, 4),
98
  "requiere_auditoria_humana": forzar_revision,
99
  "alertas_estrategicas": alertas_disparadas
100
  }
101
  except Exception as err:
102
+ db.rollback() # Si falla, deshacemos todo para mantener la integridad
103
+ logger.error(f"Error crítico en backend 500: {err}")
104
+ # Enviar el error real al frontend para depurar
105
  raise HTTPException(status_code=500, detail=str(err))
106
 
107
  @app.get("/api/v1/riesgos/cruzado")
 
108
  def obtener_riesgos_cruzados(limite_nota: float = 70.0, min_cuotas: int = 2, db: Session = Depends(get_db)):
109
  try:
110
+ # Inner Join vectorial entre la dimensión y la tabla de hechos
111
  resultados = db.query(DimEstudiante, FactRendimientoAcademico).\
112
  join(FactRendimientoAcademico, DimEstudiante.id_estudiante == FactRendimientoAcademico.id_estudiante).\
113
  filter(FactRendimientoAcademico.nota_final <= limite_nota).\
 
115
 
116
  data = []
117
  for est, fact in resultados:
118
+ # Serialización estricta bajo el contrato JSON esperado por AlertDashboard.jsx
119
  data.append({
120
  "estudiante": est.nombre_completo,
121
  "codigo": f"EST-{est.id_estudiante:06d}",
122
  "rendimiento": {
123
  "nota_actual": fact.nota_final,
124
+ "estado_academico": "CRÍTICO"
125
  },
126
  "finanzas": {
127
+ "cuotas_mora": min_cuotas,
128
+ "deuda_total": 350.0 * min_cuotas, # Proyección estática hasta integrar fact_situacion_financiera
129
  "estado_cartera": "MORA"
130
  },
131
  "nivel_riesgo_global": "ALTO - CRÍTICO"
 
133
 
134
  return {"status": "success", "data": data}
135
  except Exception as e:
136
+ logger.error(f"Fallo en la resolución del query OLAP: {e}")
137
+ raise HTTPException(status_code=500, detail="Error de lectura en el esquema estrella.")