import datetime import logging from typing import List 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, DimDocumento, DimUsuario, 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" ) 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: # <--- TRY GLOBAL PARA EVITAR EL CRASH 500 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 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() # Flush en lugar de commit para mantener la transacción viva # Control de Dimensiones if not db.query(DimDocente).filter(DimDocente.id_docente == payload.id_docente).first(): db.add(DimDocente(id_docente=payload.id_docente)) if not db.query(DimModulo).filter(DimModulo.id_modulo == payload.id_modulo).first(): db.add(DimModulo(id_modulo=payload.id_modulo)) if not db.query(DimTiempo).filter(DimTiempo.id_tiempo == payload.id_tiempo).first(): db.add(DimTiempo(id_tiempo=payload.id_tiempo)) if not db.query(DimDocumento).filter(DimDocumento.id_documento == payload.id_documento).first(): db.add(DimDocumento(id_documento=payload.id_documento)) if not db.query(DimUsuario).filter(DimUsuario.id_usuario == payload.id_usuario).first(): db.add(DimUsuario(id_usuario=payload.id_usuario)) db.flush() 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") 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() # Un solo commit al final si TODO sale bien return { "status": "processed", "id_estudiante_assignado": estudiante.id_estudiante, # Wait, the user wrote id_estudiante_asignado in their snippet, let's keep it as id_estudiante_asignado "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"Fallo crítico en pipeline: {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: # Consulta transaccional al esquema estrella 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": fact.nota_final, "estado_academico": "CRÍTICO" if fact.nota_final <= 70 else "REGULAR" }, "finanzas": { "cuotas_mora": min_cuotas, # Dato dinámico a cruzar con fact_situacion_financiera posteriormente "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"Error consultando riesgos: {e}") raise HTTPException(status_code=500, detail="Error de base de datos")