Adzacam commited on
Commit
0dee76d
·
1 Parent(s): 04b8e02

feat: expand data ingestion models to support marketing and survey pipelines and centralize fact insertion logic

Browse files
Files changed (2) hide show
  1. app.py +110 -58
  2. database.py +2 -2
app.py CHANGED
@@ -124,6 +124,20 @@ class ProcessSheetPayload(BaseModel):
124
  tipo_fuente: Optional[str] = None
125
  genero: Optional[str] = None
126
  ciudad: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  @field_validator('texto_celda')
129
  @classmethod
@@ -152,6 +166,20 @@ class ProcessSheetPayloadRaw(BaseModel):
152
  tipo_fuente: Optional[str] = None
153
  genero: Optional[str] = None
154
  ciudad: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  from typing import Union
157
 
@@ -519,20 +547,47 @@ def procesar_lote_tabular(payloads: List[ProcessSheetPayload], db: Session = Dep
519
  db.flush() # Importante: flush para poder usar los IDs recién creados
520
 
521
  # Hecho (La data de payload ya viene corregida por ti desde el frontend)
522
- nuevo_hecho = FactRendimientoAcademico(
523
- id_estudiante=estudiante.id_estudiante,
524
- id_docente=payload.id_docente,
525
- id_modulo=payload.id_modulo,
526
- id_tiempo=payload.id_tiempo,
527
- id_documento=payload.id_documento,
528
- id_usuario_carga=payload.id_usuario,
529
- nota_final=payload.nota_detectada,
530
- asistencia_pct=payload.asistencia,
531
- incumplimiento_actividades_pct=payload.incumplimiento_tareas,
532
- nivel_confianza_ia=confianza_ia,
533
- requiere_revision=forzar_revision
534
- )
535
- db.add(nuevo_hecho)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
 
537
  # Al final, guardamos todo junto
538
  db.commit()
@@ -1199,9 +1254,18 @@ def batch_analyze_nlp(
1199
  existing_users = {u.id for u in db.query(Users).all()}
1200
 
1201
  for record in records:
1202
- # Extraer NLP
 
 
 
 
1203
  entidades = ner_engine.extract_entities(record.texto_celda)
1204
- confianza_ia = sum([e["score"] for e in entidades]) / len(entidades) if entidades else 1.0
 
 
 
 
 
1205
  nombre_resuelto = record.texto_celda[:200].strip()
1206
 
1207
  # Consultar log_auditoria_nlp primero
@@ -1224,25 +1288,35 @@ def batch_analyze_nlp(
1224
  db.flush()
1225
  estudiantes_existentes.append(estudiante)
1226
  else:
1227
- best_match, score = find_best_match(nombre_resuelto, estudiantes_existentes)
1228
- if best_match and score >= 0.8:
1229
- estudiante = best_match
1230
- if score < confianza_ia:
1231
- confianza_ia = score
 
 
 
 
 
 
 
1232
  else:
1233
- estudiante = DimEstudiante(nombre_completo=nombre_resuelto, codigo_estudiante=record.codigo_estudiante, genero=record.genero, ciudad=record.ciudad)
1234
- db.add(estudiante)
1235
- db.flush()
1236
- estudiantes_existentes.append(estudiante)
 
 
1237
 
1238
- candidatos_difusos = get_top_matches(nombre_resuelto, estudiantes_existentes, top_k=5) if requiere_revision or confianza_ia < 0.60 else []
1239
 
1240
  # Calculo de alertas
1241
  alertas = []
1242
- if record.nota_detectada <= 70.0:
1243
- alertas.append("RIESGO_ACADEMICO_CRITICO")
1244
- if record.asistencia < 70.0 or record.incumplimiento_tareas > 30.0:
1245
- alertas.append("RIESGO_DESERCION_ALTA")
 
1246
 
1247
  # Ensure dimensions exist using in-memory cache to prevent lock contention
1248
  docente_name = getattr(record, 'docente', None) or "Docente Generico"
@@ -1281,7 +1355,7 @@ def batch_analyze_nlp(
1281
  db.flush()
1282
 
1283
  requiere_revision = False
1284
- if confianza_ia < 0.60:
1285
  log = LogAuditoriaNlp(
1286
  texto_original=nombre_resuelto,
1287
  prediccion_beto=nombre_resuelto,
@@ -1293,32 +1367,9 @@ def batch_analyze_nlp(
1293
  db.flush()
1294
  requiere_revision = True
1295
 
1296
- # Insert into Constellation Schema ALWAYS
1297
- if record.tipo_fuente == "FINANCE":
1298
- fact = FactSituacionFinanciera(
1299
- id_estudiante=estudiante.id_estudiante,
1300
- id_tiempo=id_tiempo_val,
1301
- monto_deuda=getattr(record, 'monto_deuda', 0),
1302
- cuotas_impagas=getattr(record, 'cuotas_impagas', 0),
1303
- estado_cartera="AL_DIA",
1304
- tipo_alerta="NINGUNA"
1305
- )
1306
- db.add(fact)
1307
- else:
1308
- fact = FactRendimientoAcademico(
1309
- id_estudiante=estudiante.id_estudiante,
1310
- id_docente=id_docente_val,
1311
- id_modulo=id_modulo_val,
1312
- id_tiempo=id_tiempo_val,
1313
- id_documento=id_documento_val,
1314
- id_usuario_carga=id_usuario_val,
1315
- nota_final=record.nota_detectada,
1316
- asistencia_pct=record.asistencia,
1317
- incumplimiento_actividades_pct=record.incumplimiento_tareas,
1318
- nivel_confianza_ia=confianza_ia,
1319
- requiere_revision=requiere_revision
1320
- )
1321
- db.add(fact)
1322
 
1323
  results.append({
1324
  "anonymized_name": anonymize_name(nombre_resuelto),
@@ -1327,7 +1378,8 @@ def batch_analyze_nlp(
1327
  "alertas": alertas,
1328
  "requiere_revision": requiere_revision,
1329
  "status": "pending_human_review" if requiere_revision else "inserted",
1330
- "candidatos_difusos": candidatos_difusos
 
1331
  })
1332
 
1333
  try:
 
124
  tipo_fuente: Optional[str] = None
125
  genero: Optional[str] = None
126
  ciudad: Optional[str] = None
127
+
128
+ # Financial fields
129
+ monto_deuda: Optional[float] = 0.0
130
+ cuotas_impagas: Optional[int] = 0
131
+
132
+ # Marketing fields
133
+ leads: Optional[int] = 0
134
+ reservas: Optional[int] = 0
135
+ inscritos: Optional[int] = 0
136
+ costo: Optional[float] = 0.0
137
+
138
+ # Survey fields
139
+ pregunta: Optional[str] = None
140
+ puntuacion: Optional[float] = 0.0
141
 
142
  @field_validator('texto_celda')
143
  @classmethod
 
166
  tipo_fuente: Optional[str] = None
167
  genero: Optional[str] = None
168
  ciudad: Optional[str] = None
169
+
170
+ # Financial fields
171
+ monto_deuda: Optional[float] = 0.0
172
+ cuotas_impagas: Optional[int] = 0
173
+
174
+ # Marketing fields
175
+ leads: Optional[int] = 0
176
+ reservas: Optional[int] = 0
177
+ inscritos: Optional[int] = 0
178
+ costo: Optional[float] = 0.0
179
+
180
+ # Survey fields
181
+ pregunta: Optional[str] = None
182
+ puntuacion: Optional[float] = 0.0
183
 
184
  from typing import Union
185
 
 
547
  db.flush() # Importante: flush para poder usar los IDs recién creados
548
 
549
  # Hecho (La data de payload ya viene corregida por ti desde el frontend)
550
+ # Determinar area (Fallback a ACADEMIC si no se provee)
551
+ area = getattr(payload, 'tipo_fuente', 'ACADEMIC')
552
+ if not area:
553
+ area = 'ACADEMIC'
554
+
555
+ if area == "MARKETING":
556
+ nuevo_hecho = FactMarketingInscripciones(
557
+ id_modulo=payload.id_modulo,
558
+ id_tiempo=payload.id_tiempo,
559
+ leads=getattr(payload, 'leads', 1),
560
+ reservas=getattr(payload, 'reservas', 0),
561
+ inscritos=getattr(payload, 'inscritos', 0),
562
+ costo_programa=getattr(payload, 'costo', 0)
563
+ )
564
+ db.add(nuevo_hecho)
565
+ elif area == "SURVEYS":
566
+ nuevo_hecho = FactEvaluacionDocente(
567
+ id_docente=payload.id_docente,
568
+ id_modulo=payload.id_modulo,
569
+ id_estudiante=estudiante.id_estudiante,
570
+ id_tiempo=payload.id_tiempo,
571
+ pregunta_bloque=getattr(payload, 'pregunta', 'General'),
572
+ puntuacion=getattr(payload, 'puntuacion', 5.0),
573
+ comentario=nombre_resuelto
574
+ )
575
+ db.add(nuevo_hecho)
576
+ else: # ACADEMIC
577
+ nuevo_hecho = FactRendimientoAcademico(
578
+ id_estudiante=estudiante.id_estudiante,
579
+ id_docente=payload.id_docente,
580
+ id_modulo=payload.id_modulo,
581
+ id_tiempo=payload.id_tiempo,
582
+ id_documento=payload.id_documento,
583
+ id_usuario_carga=payload.id_usuario,
584
+ nota_final=payload.nota_detectada,
585
+ asistencia_pct=payload.asistencia,
586
+ incumplimiento_actividades_pct=payload.incumplimiento_tareas,
587
+ nivel_confianza_ia=confianza_ia,
588
+ requiere_revision=forzar_revision
589
+ )
590
+ db.add(nuevo_hecho)
591
 
592
  # Al final, guardamos todo junto
593
  db.commit()
 
1254
  existing_users = {u.id for u in db.query(Users).all()}
1255
 
1256
  for record in records:
1257
+ # 1. Determinar el área y calcular confianza ajustada
1258
+ area = getattr(record, 'tipo_fuente', 'ACADEMIC')
1259
+ if not area:
1260
+ area = 'ACADEMIC'
1261
+
1262
  entidades = ner_engine.extract_entities(record.texto_celda)
1263
+ if entidades:
1264
+ confianza_ia = sum([e["score"] for e in entidades]) / len(entidades)
1265
+ else:
1266
+ # Para áreas no académicas, la falta de entidades persona no es penalizable severamente
1267
+ confianza_ia = 0.85 if area != 'ACADEMIC' else 0.40
1268
+
1269
  nombre_resuelto = record.texto_celda[:200].strip()
1270
 
1271
  # Consultar log_auditoria_nlp primero
 
1288
  db.flush()
1289
  estudiantes_existentes.append(estudiante)
1290
  else:
1291
+ # Skip fuzzy match for non-academic/non-finance areas where "student" name isn't critical
1292
+ if area in ['ACADEMIC', 'FINANCE']:
1293
+ best_match, score = find_best_match(nombre_resuelto, estudiantes_existentes)
1294
+ if best_match and score >= 0.8:
1295
+ estudiante = best_match
1296
+ if score < confianza_ia:
1297
+ confianza_ia = score
1298
+ else:
1299
+ estudiante = DimEstudiante(nombre_completo=nombre_resuelto, codigo_estudiante=record.codigo_estudiante, genero=record.genero, ciudad=record.ciudad)
1300
+ db.add(estudiante)
1301
+ db.flush()
1302
+ estudiantes_existentes.append(estudiante)
1303
  else:
1304
+ # Mock student for MARKETING/SURVEYS if none exists to satisfy foreign keys
1305
+ estudiante = estudiantes_existentes[0] if estudiantes_existentes else DimEstudiante(nombre_completo="Anonimo")
1306
+ if not estudiantes_existentes:
1307
+ db.add(estudiante)
1308
+ db.flush()
1309
+ estudiantes_existentes.append(estudiante)
1310
 
1311
+ candidatos_difusos = get_top_matches(nombre_resuelto, estudiantes_existentes, top_k=5) if requiere_revision or (confianza_ia < 0.60 and area in ['ACADEMIC', 'FINANCE']) else []
1312
 
1313
  # Calculo de alertas
1314
  alertas = []
1315
+ if area == 'ACADEMIC':
1316
+ if getattr(record, 'nota_detectada', 100) <= 70.0:
1317
+ alertas.append("RIESGO_ACADEMICO_CRITICO")
1318
+ if getattr(record, 'asistencia', 100) < 70.0 or getattr(record, 'incumplimiento_tareas', 0) > 30.0:
1319
+ alertas.append("RIESGO_DESERCION_ALTA")
1320
 
1321
  # Ensure dimensions exist using in-memory cache to prevent lock contention
1322
  docente_name = getattr(record, 'docente', None) or "Docente Generico"
 
1355
  db.flush()
1356
 
1357
  requiere_revision = False
1358
+ if confianza_ia < 0.60 and area in ['ACADEMIC', 'FINANCE']:
1359
  log = LogAuditoriaNlp(
1360
  texto_original=nombre_resuelto,
1361
  prediccion_beto=nombre_resuelto,
 
1367
  db.flush()
1368
  requiere_revision = True
1369
 
1370
+ # Insert into Constellation Schema ALWAYS based on area
1371
+ # ELIMINADO: La inserción a las tablas de hechos ahora OCURRE ÚNICAMENTE en /api/v1/ingesta/bulk
1372
+ # para evitar duplicación de datos entre el análisis y la confirmación final.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1373
 
1374
  results.append({
1375
  "anonymized_name": anonymize_name(nombre_resuelto),
 
1378
  "alertas": alertas,
1379
  "requiere_revision": requiere_revision,
1380
  "status": "pending_human_review" if requiere_revision else "inserted",
1381
+ "candidatos_difusos": candidatos_difusos,
1382
+ "area_asignada": area
1383
  })
1384
 
1385
  try:
database.py CHANGED
@@ -171,7 +171,7 @@ class FactEvaluacionDocente(Base):
171
 
172
  class FactMarketingInscripciones(Base):
173
  """Métricas de OKRs de Marketing y ventas."""
174
- __tablename__ = "fact_marketing_inscripciones"
175
  id_hecho_mkt = Column(Integer, primary_key=True)
176
  id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
177
  id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
@@ -182,7 +182,7 @@ class FactMarketingInscripciones(Base):
182
 
183
  class FactRentabilidadPresupuesto(Base):
184
  """Indicadores de Rentabilidad, Egresos y EBITDA (Ejecutado vs Meta)."""
185
- __tablename__ = "fact_rentabilidad_presupuesto"
186
  id_hecho_rent = Column(Integer, primary_key=True)
187
  id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
188
  id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
 
171
 
172
  class FactMarketingInscripciones(Base):
173
  """Métricas de OKRs de Marketing y ventas."""
174
+ __tablename__ = "fact_marketing"
175
  id_hecho_mkt = Column(Integer, primary_key=True)
176
  id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
177
  id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))
 
182
 
183
  class FactRentabilidadPresupuesto(Base):
184
  """Indicadores de Rentabilidad, Egresos y EBITDA (Ejecutado vs Meta)."""
185
+ __tablename__ = "fact_rentabilidad"
186
  id_hecho_rent = Column(Integer, primary_key=True)
187
  id_modulo = Column(Integer, ForeignKey("dim_modulo.id_modulo"))
188
  id_tiempo = Column(Integer, ForeignKey("dim_tiempo.id_tiempo"))