# -*- 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)