Spaces:
Sleeping
Sleeping
| 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 | |
| def read_root(): | |
| return { | |
| "status": "healthy", | |
| "service": "GiraGroup BI Backend API Cloud", | |
| "ner_initialized": ner_engine._initialized or ner_engine.pipeline is not None | |
| } | |
| 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)) | |
| 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)) |