diff --git a/agent-office/app/agents/lotto.py b/agent-office/app/agents/lotto.py index f6aa461..2301fc1 100644 --- a/agent-office/app/agents/lotto.py +++ b/agent-office/app/agents/lotto.py @@ -30,8 +30,7 @@ class LottoAgent(BaseAgent): async def run_signal_check(self, source: str = "light") -> dict: """비-LLM 시그널 평가 (light/sim) 또는 deep_check (LLM 호출 후). - Phase 2: 텔레그램 발송 안 함. lotto_signals INSERT만. - Phase 3 (Task 9): urgent 시그널 텔레그램 발송 + throttle 추가. + Phase 3 (Task 9): urgent 시그널 텔레그램 발송 + throttle/daily-cap 추가. """ from ..curator.signal_runner import run_signal_check from ..config import LOTTO_Z_NORMAL, LOTTO_Z_URGENT @@ -62,17 +61,86 @@ class LottoAgent(BaseAgent): self.agent_id, f"signal_check({source}) → overall={outcome['overall_fire']} results={len(outcome['results'])}", ) + + # --- Throttle + 텔레그램 urgent 발송 --- + from ..config import LOTTO_THROTTLE_HOURS, LOTTO_URGENT_DAILY_MAX + from ..db import ( + get_last_signal_notification, get_recent_urgent_count, + mark_signal_notified, + ) + from ..notifiers.telegram_lotto import send_urgent_signal + + if outcome["overall_fire"] == "urgent": + if get_recent_urgent_count(hours=24) >= LOTTO_URGENT_DAILY_MAX: + add_log( + self.agent_id, + "urgent daily cap 도달 → normal로 강등 (digest 합류)", + level="warning", + ) + else: + blocked = False + for r in outcome["results"]: + if r["fire_level"] in ("normal", "urgent"): + last = get_last_signal_notification( + metric=r["metric"], fire_level=r["fire_level"], + hours=LOTTO_THROTTLE_HOURS, + ) + if last: + blocked = True + break + if not blocked: + from datetime import datetime, timezone + event = { + "fire_level": "urgent", + "triggered_at": datetime.now(timezone.utc).isoformat(), + "results": outcome["results"], + } + await send_urgent_signal(event) + for r in outcome["results"]: + if r["fire_level"] in ("normal", "urgent"): + mark_signal_notified(r["signal_id"]) + add_log(self.agent_id, f"urgent 텔레그램 발송 완료 (시그널 {len(outcome['results'])}개 마킹)") + return {"ok": True, **outcome} except Exception as e: add_log(self.agent_id, f"signal_check 예외: {e}", level="error") return {"ok": False, "message": f"{type(e).__name__}: {e}"} async def run_daily_digest(self) -> dict: - """Phase 2: 발화 카운트만 반환. Phase 3 (Task 9)에서 텔레그램 발송 추가.""" - from ..db import get_recent_lotto_signals, add_log + """일일 요약 — 지난 24h normal/urgent 발화를 묶어 텔레그램 1통.""" + from ..db import ( + get_recent_lotto_signals, get_signals_history, add_log, + get_baseline, + ) + from ..notifiers.telegram_lotto import send_signal_summary + sigs = get_recent_lotto_signals(hours=24, min_fire="normal") - add_log(self.agent_id, f"daily_digest: 지난 24h 발화 {len(sigs)}건") - return {"ok": True, "count": len(sigs), "signals": sigs} + total_24h = get_signals_history(days=1) + evaluated = len(total_24h) + + # weights_trend: drift_weights_cache의 prev/curr 차이 + trend = {} + try: + cache = get_baseline("drift_weights_cache") + if cache and isinstance(cache["window_values"], list) and len(cache["window_values"]) >= 2: + prev_w = cache["window_values"][-2] + curr_w = cache["window_values"][-1] + trend = { + k: curr_w.get(k, 0.0) - prev_w.get(k, 0.0) + for k in (set(prev_w) | set(curr_w)) + } + except Exception as e: + add_log(self.agent_id, f"weights_trend 계산 실패: {e}", level="warning") + + digest = { + "evaluated": evaluated, + "fired": len(sigs), + "signals": sigs, + "weights_trend": trend, + } + await send_signal_summary(digest) + add_log(self.agent_id, f"daily_digest 발송: 평가 {evaluated} / 발화 {len(sigs)}") + return {"ok": True, **digest} async def _run(self, source: str) -> dict: task_id = create_task(self.agent_id, "curate_weekly", {"source": source}) diff --git a/agent-office/app/curator/signal_runner.py b/agent-office/app/curator/signal_runner.py index 013cca6..e7775f3 100644 --- a/agent-office/app/curator/signal_runner.py +++ b/agent-office/app/curator/signal_runner.py @@ -80,6 +80,8 @@ def evaluate_metric_and_persist( "signal_id": sid, "metric": metric, "value": value, + "baseline_mu": bl.mu if bl.size > 0 else None, + "baseline_sigma": bl.sigma if bl.size >= 2 else None, "z_score": z, "fire_level": fire, "payload": payload or {}, diff --git a/agent-office/app/notifiers/telegram_lotto.py b/agent-office/app/notifiers/telegram_lotto.py index f900c7b..8673e51 100644 --- a/agent-office/app/notifiers/telegram_lotto.py +++ b/agent-office/app/notifiers/telegram_lotto.py @@ -88,10 +88,11 @@ def _format_urgent_signal(event: Dict[str, Any]) -> str: mu = r.get("baseline_mu") sigma = r.get("baseline_sigma") z = r.get("z_score") + v_text = f"{v:.2f}" if v is not None else "N/A" if mu is not None and sigma is not None and z is not None: - lines.append(f"• {label} {v:.2f} (μ={mu:.2f}, σ={sigma:.2f}) z={z:.1f}") + lines.append(f"• {label} {v_text} (μ={mu:.2f}, σ={sigma:.2f}) z={z:.1f}") else: - lines.append(f"• {label} {v:.2f}") + lines.append(f"• {label} {v_text}") # drift 페이로드 — 어떤 전략이 변동했는지 한 줄 for r in fired: