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, 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 resto del código de ProcessSheetPayload y endpoints se mantiene exactamente igual... 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)): 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 = False if confianza_ia < 0.60: forzar_revision = True 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.commit() db.refresh(estudiante) 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") try: 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"Fallo en persistencia: {err}") raise HTTPException(status_code=500, detail="Error al escribir en Supabase.") @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")