Files
INFRAESTRUCTURA/scripts/monitor_estado/ciclo.py
2026-10-03 17:13:39 +02:00

134 lines
5.0 KiB
Python

# -*- coding: utf-8 -*-
"""ciclo.py — Orquestador de un ciclo del monitor (RF-1..RF-18, RF-23, RF-24).
Ejecuta la recogida de todas las fuentes, el archivado por fuente, el resumen IA,
el aviso por voz y el mantenimiento (poda, marca de ultimo ciclo). Aisla los errores
por fuente: una fuente caida no aborta el ciclo (RF-6).
Las dependencias externas (fuentes, IA, aviso) son inyectables para poder testear.
"""
from __future__ import annotations
from datetime import date, datetime, timedelta
from . import archivo, aviso, config, resumen, vistos
from .fuentes import correo as correo_mod
from .fuentes import jira as jira_mod
from .fuentes import telegram as telegram_mod
def _desde_jira(ahora: datetime) -> datetime:
"""Instante desde el que buscar incidencias nuevas: ultimo ciclo o, si no, 24 h."""
ultimo = archivo.leer_ultimo_ciclo()
inicio = ultimo.get("inicio")
if inicio:
try:
return datetime.fromisoformat(inicio)
except Exception:
pass
return ahora - timedelta(hours=24)
def ejecutar(cfg: dict | None = None, hoy: date | None = None, deps: dict | None = None) -> dict:
"""Ejecuta un ciclo completo. Devuelve el resumen estructurado del ciclo."""
cfg = cfg or config.cargar()
ahora = datetime.now()
hoy = hoy or ahora.date()
deps = deps or {}
telegram_recoger = deps.get("telegram_recoger", telegram_mod.recoger_todas)
correo_recoger = deps.get("correo_recoger", correo_mod.recoger_recientes)
jira_cliente = deps.get("jira_cliente")
ia_fn = deps.get("ia")
avisar_fn = deps.get("avisar", aviso.avisar)
novedades: dict[str, list] = {}
errores: list[tuple[str, str]] = []
# --- Telegram ---
try:
resultado, errs = telegram_recoger(cfg, hoy)
for nombre, msgs in resultado.items():
archivo.escribe_dia(
f"telegram/{nombre}", hoy, msgs,
id_fn=lambda m: m["id"], render_fn=telegram_mod.render_mensaje,
titulo=f"{nombre} — {hoy.isoformat()}",
)
nuevos = vistos.filtrar_nuevos(f"telegram_{nombre}", msgs, id_fn=lambda m: m["id"])
if nuevos:
novedades[f"telegram/{nombre}"] = nuevos
errores += [(f"telegram/{n}", e) for n, e in errs]
except Exception as e:
errores.append(("telegram", str(e)))
# --- Correo ---
try:
correos = correo_recoger(horas=cfg.get("correo", {}).get("horas", 24))
archivo.escribe_dia("correo", hoy, correos, id_fn=lambda m: m["id"], render_fn=correo_mod.render_correo)
nuevos = vistos.filtrar_nuevos("correo", correos, id_fn=lambda m: m["id"])
if nuevos:
novedades["correo"] = nuevos
except Exception as e:
errores.append(("correo", str(e)))
# --- Jira (solo lectura) ---
try:
cliente = jira_cliente or jira_mod.construir_cliente(cfg)
ds_nuevos = jira_mod.ds_nuevos(cliente, _desde_jira(ahora))
ds_sin_asignar = jira_mod.ds_sin_asignar(cliente)
todos = ds_nuevos + ds_sin_asignar
archivo.escribe_dia("jira", hoy, todos, id_fn=lambda m: m["key"], render_fn=jira_mod.render_jira)
nuevos = vistos.filtrar_nuevos("jira", todos, id_fn=lambda m: m["key"])
if nuevos:
novedades["jira"] = nuevos
except Exception as e:
errores.append(("jira", str(e)))
# --- Resumen IA + clasificacion ---
res = resumen.generar(novedades, errores, cfg=cfg, _ia=ia_fn)
archivo.escribe_resumen(res)
# --- Aviso por voz ---
emitido = avisar_fn(res, cfg=cfg, _runner=deps.get("aviso_runner"))
res["aviso_emitido"] = emitido
# --- Mantenimiento ---
archivo.poda(cfg.get("retencion_dias", 30), hoy)
archivo.escribe_ultimo_ciclo({
"inicio": ahora.isoformat(),
"fin": datetime.now().isoformat(),
"ok": not errores,
"fuentes_con_novedad": res.get("fuentes_con_novedad", []),
"errores": res.get("errores", []),
})
return res
def texto_estado() -> str:
"""Texto legible del ultimo resumen y estado, para el comando `estado` (RF-18)."""
r = archivo.leer_ultimo_ciclo()
resumen_json = {}
ruta = archivo.MONITOR_DIR / "resumen_actual.json"
if ruta.exists():
import json
try:
resumen_json = json.loads(ruta.read_text(encoding="utf-8"))
except Exception:
resumen_json = {}
lineas = [
"── MONITOR DE ESTADO ──",
f"Datos en : {archivo.MONITOR_DIR}",
f"Ultimo ciclo : {r.get('inicio', 'nunca')} (ok={r.get('ok', '-')})",
]
if r.get("errores"):
lineas.append(f"Errores : {len(r['errores'])}")
lineas.append("── Ultimo resumen ──")
lineas.append(resumen_json.get("resumen_md", "(sin resumen)") or "(sin resumen)")
atencion = resumen_json.get("requiere_atencion", [])
if atencion:
lineas.append("Requiere atencion:")
for a in atencion:
lineas.append(f" - [{a.get('fuente', '?')}] {a.get('texto', '')} ({a.get('motivo', '')})")
return "\n".join(lineas)