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

Fix: Transacciones seguras con db.flush()

Browse files
Files changed (2) hide show
  1. app.py +32 -46
  2. database.py +11 -11
app.py CHANGED
@@ -37,51 +37,38 @@ 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
- # 1. Extracción con BETO NLP
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
 
44
- forzar_revision = False
45
- if confianza_ia < 0.60:
46
- forzar_revision = True
 
 
 
47
 
48
- # 2. Resolución de Estudiante
49
- nombre_resuelto = payload.texto_celda.strip()
50
- estudiante = db.query(DimEstudiante).filter(DimEstudiante.nombre_completo == nombre_resuelto).first()
51
- if not estudiante:
52
- estudiante = DimEstudiante(nombre_completo=nombre_resuelto)
53
- db.add(estudiante)
54
- db.commit()
55
- db.refresh(estudiante)
56
-
57
- # 3. Control de Dimensiones (Evitar violación de Foreign Keys)
58
- # Autogenerar IDs por defecto si no existen
59
- if not db.query(DimDocente).filter(DimDocente.id_docente == payload.id_docente).first():
60
- db.add(DimDocente(id_docente=payload.id_docente, nombre="Docente Generico", especialidad="Generico"))
61
-
62
- if not db.query(DimModulo).filter(DimModulo.id_modulo == payload.id_modulo).first():
63
- db.add(DimModulo(id_modulo=payload.id_modulo, nombre_modulo="Modulo Generico", version="1.0"))
64
-
65
- if not db.query(DimTiempo).filter(DimTiempo.id_tiempo == payload.id_tiempo).first():
66
- db.add(DimTiempo(id_tiempo=payload.id_tiempo, anio=2026, mes=5, dia=30, trimestre=2))
67
-
68
- if not db.query(DimDocumento).filter(DimDocumento.id_documento == payload.id_documento).first():
69
- db.add(DimDocumento(id_documento=payload.id_documento, tipo_documento="Consolidado Generico"))
70
-
71
- if not db.query(DimUsuario).filter(DimUsuario.id_usuario == payload.id_usuario).first():
72
- db.add(DimUsuario(id_usuario=payload.id_usuario, rol="Sistema", nombre="Admin"))
73
-
74
- db.commit() # Asegurar que las FK existan antes del INSERT principal
75
 
76
- # 4. Alertas Estratégicas
77
- alertas_disparadas = []
78
- if payload.nota_detectada <= 70.0:
79
- alertas_disparadas.append("RIESGO_ACADEMICO_CRITICO")
80
- if payload.asistencia < 70.0 or payload.incumplimiento_tareas > 30.0:
81
- alertas_disparadas.append("RIESGO_DESERCION_ALTA")
82
 
83
- # 5. Persistencia del Hecho (Fact)
84
- try:
85
  nuevo_hecho = FactRendimientoAcademico(
86
  id_estudiante=estudiante.id_estudiante,
87
  id_docente=payload.id_docente,
@@ -96,20 +83,19 @@ def procesar_registro_tabular(payload: ProcessSheetPayload, db: Session = Depend
96
  requiere_revision=forzar_revision
97
  )
98
  db.add(nuevo_hecho)
99
- db.commit()
100
 
101
  return {
102
  "status": "processed",
103
- "id_estudiante_asignado": estudiante.id_estudiante,
104
  "confianza_modelo_beto": round(confianza_ia, 4),
105
  "requiere_auditoria_humana": forzar_revision,
106
  "alertas_estrategicas": alertas_disparadas
107
  }
108
  except Exception as err:
109
  db.rollback()
110
- logger.error(f"Fallo en persistencia: {err}")
111
- # Retornar detalles del error en consola puede ser útil para ti, aunque en producción no se expone el log del motor
112
- raise HTTPException(status_code=500, detail=f"Error al escribir en Supabase: {str(err)}")
113
 
114
  @app.get("/api/v1/riesgos/cruzado")
115
 
 
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:
68
+ alertas_disparadas.append("RIESGO_ACADEMICO_CRITICO")
69
+ if payload.asistencia < 70.0 or payload.incumplimiento_tareas > 30.0:
70
+ alertas_disparadas.append("RIESGO_DESERCION_ALTA")
 
71
 
 
 
72
  nuevo_hecho = FactRendimientoAcademico(
73
  id_estudiante=estudiante.id_estudiante,
74
  id_docente=payload.id_docente,
 
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
 
database.py CHANGED
@@ -35,33 +35,33 @@ class DimEstudiante(Base):
35
  class DimDocente(Base):
36
  __tablename__ = "dim_docente"
37
  id_docente = Column(Integer, primary_key=True)
38
- nombre = Column(String(200), nullable=False)
39
- especialidad = Column(String(100))
40
 
41
  class DimModulo(Base):
42
  __tablename__ = "dim_modulo"
43
  id_modulo = Column(Integer, primary_key=True)
44
- nombre_modulo = Column(String(200), nullable=False)
45
- version = Column(String(50))
46
 
47
  class DimTiempo(Base):
48
  __tablename__ = "dim_tiempo"
49
  id_tiempo = Column(Integer, primary_key=True)
50
- anio = Column(Integer, nullable=False)
51
- mes = Column(Integer, nullable=False)
52
- dia = Column(Integer, nullable=False)
53
- trimestre = Column(Integer, nullable=False)
54
 
55
  class DimDocumento(Base):
56
  __tablename__ = "dim_documento"
57
  id_documento = Column(Integer, primary_key=True)
58
- tipo_documento = Column(String(100), nullable=False)
59
 
60
  class DimUsuario(Base):
61
  __tablename__ = "dim_usuario"
62
  id_usuario = Column(Integer, primary_key=True)
63
- rol = Column(String(50), nullable=False)
64
- nombre = Column(String(100), nullable=False)
65
 
66
  class FactRendimientoAcademico(Base):
67
  __tablename__ = "fact_rendimiento_academico"
 
35
  class DimDocente(Base):
36
  __tablename__ = "dim_docente"
37
  id_docente = Column(Integer, primary_key=True)
38
+ nombre = Column(String(200), nullable=False, default="Docente Generico")
39
+ especialidad = Column(String(100), default="Generico")
40
 
41
  class DimModulo(Base):
42
  __tablename__ = "dim_modulo"
43
  id_modulo = Column(Integer, primary_key=True)
44
+ nombre_modulo = Column(String(200), nullable=False, default="Modulo Generico")
45
+ version = Column(String(50), default="1.0")
46
 
47
  class DimTiempo(Base):
48
  __tablename__ = "dim_tiempo"
49
  id_tiempo = Column(Integer, primary_key=True)
50
+ anio = Column(Integer, nullable=False, default=2026)
51
+ mes = Column(Integer, nullable=False, default=5)
52
+ dia = Column(Integer, nullable=False, default=30)
53
+ trimestre = Column(Integer, nullable=False, default=2)
54
 
55
  class DimDocumento(Base):
56
  __tablename__ = "dim_documento"
57
  id_documento = Column(Integer, primary_key=True)
58
+ tipo_documento = Column(String(100), nullable=False, default="Consolidado Generico")
59
 
60
  class DimUsuario(Base):
61
  __tablename__ = "dim_usuario"
62
  id_usuario = Column(Integer, primary_key=True)
63
+ rol = Column(String(50), nullable=False, default="Sistema")
64
+ nombre = Column(String(100), nullable=False, default="Admin")
65
 
66
  class FactRendimientoAcademico(Base):
67
  __tablename__ = "fact_rendimiento_academico"