from .base import BaseAgent from ..db import create_task, update_task_status, add_log from ..curator.pipeline import curate_weekly, CuratorError class LottoAgent(BaseAgent): agent_id = "lotto" display_name = "로또 큐레이터" async def on_schedule(self) -> None: if self.state != "idle": return await self._run(source="auto") async def on_command(self, action: str, params: dict) -> dict: if action in ("curate_now", "curate_weekly"): return await self._run(source="manual") if action == "status": return {"ok": True, "message": f"{self.state}: {self.state_detail}"} if action in ("signal_check", "light_check", "sim_check", "deep_check"): source = action.replace("_check", "") if action != "signal_check" else "light" return await self.run_signal_check(source=source) if action == "daily_digest": return await self.run_daily_digest() return {"ok": False, "message": f"unknown action: {action}"} async def on_approval(self, task_id: str, approved: bool, feedback: str = "") -> None: pass async def run_signal_check(self, source: str = "light") -> dict: """비-LLM 시그널 평가. task_id wrap 적용.""" from ..curator.signal_runner import run_signal_check from ..config import ( LOTTO_Z_NORMAL, LOTTO_Z_URGENT, LOTTO_THROTTLE_HOURS, LOTTO_URGENT_DAILY_MAX, ) from ..db import ( create_task, update_task_status, add_log, get_last_signal_notification, get_recent_urgent_count, mark_signal_notified, ) from ..notifiers.telegram_lotto import send_urgent_signal from ..service_proxy import lotto_latest_draw if self.state not in ("idle", "reporting"): return {"ok": False, "message": f"busy ({self.state})"} task_id = create_task("lotto", "signal_check", {"source": source}) try: curate_result = None current_draw_no = await lotto_latest_draw() if source == "deep": from ..curator.pipeline import curate_weekly cw = await curate_weekly(source="signal_deep") curate_result = {"confidence": cw.get("confidence")} if cw.get("draw_no"): current_draw_no = cw.get("draw_no") outcome = await run_signal_check( source=source, z_normal=LOTTO_Z_NORMAL, z_urgent=LOTTO_Z_URGENT, curate_result=curate_result, current_draw_no=current_draw_no, ) # urgent 텔레그램 + throttle (기존 동작 유지) if outcome["overall_fire"] == "urgent": if get_recent_urgent_count(hours=24) >= LOTTO_URGENT_DAILY_MAX: add_log("lotto", "urgent daily cap 도달 → normal로 강등", level="warning", task_id=task_id) else: blocked = False for r in outcome["results"]: if r["fire_level"] in ("normal", "urgent"): if get_last_signal_notification( metric=r["metric"], fire_level=r["fire_level"], hours=LOTTO_THROTTLE_HOURS, ): 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("lotto", f"urgent 텔레그램 발송 ({len(outcome['results'])}개 시그널)", task_id=task_id) fired_metrics = [ r["metric"] for r in outcome["results"] if r["fire_level"] not in ("noop", "warmup") ] update_task_status(task_id, "succeeded", result_data={ "source": source, "overall_fire": outcome["overall_fire"], "n_results": len(outcome["results"]), "fired_metrics": fired_metrics, }) add_log("lotto", f"signal_check({source}) → {outcome['overall_fire']} results={len(outcome['results'])}", task_id=task_id) return {"ok": True, **outcome} except Exception as e: update_task_status(task_id, "failed", result_data={"error": str(e)}) add_log("lotto", f"signal_check 예외: {e}", level="error", task_id=task_id) return {"ok": False, "message": f"{type(e).__name__}: {e}"} async def run_daily_digest(self) -> dict: """일일 요약 — 지난 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") 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_weekly_evolution_report(self) -> dict: """토 22:15 — lotto-lab evaluate-now 트리거 후 텔레그램 리포트.""" from ..service_proxy import lotto_evolver_evaluate, lotto_evolver_status from ..notifiers.telegram_lotto import send_evolution_report from ..db import add_log try: eval_result = await lotto_evolver_evaluate() status = await lotto_evolver_status() current_base = status.get("current_base") or [0.2] * 5 await send_evolution_report(eval_result, current_base) add_log( self.agent_id, f"weekly_evolution_report 발송: draw={eval_result.get('draw_no')} reason={eval_result.get('update_reason')}", ) return {"ok": True, **eval_result} except Exception as e: add_log(self.agent_id, f"weekly_evolution_report 예외: {e}", level="error") return {"ok": False, "message": f"{type(e).__name__}: {e}"} async def _run(self, source: str) -> dict: task_id = create_task(self.agent_id, "curate_weekly", {"source": source}) await self.transition("working", "후보 수집 및 AI 큐레이션 중...", task_id) try: result = await curate_weekly(source=source) update_task_status(task_id, "succeeded", result_data={ k: v for k, v in result.items() if k != "payload" }) await self.transition("reporting", f"#{result['draw_no']} 브리핑 저장 완료") add_log(self.agent_id, f"큐레이션 완료: #{result['draw_no']} conf={result['confidence']}", task_id=task_id) # 텔레그램 헤드라인 푸시 (실패해도 큐레이션은 성공으로 마감) try: from ..notifiers.telegram_lotto import send_curator_briefing await send_curator_briefing(result["payload"]) except Exception as e: add_log(self.agent_id, f"텔레그램 알림 실패: {e}", level="warning", task_id=task_id) await self.transition("idle", "대기 중") return {"ok": True, **{k: v for k, v in result.items() if k != "payload"}} except CuratorError as e: update_task_status(task_id, "failed", result_data={"error": str(e)}) add_log(self.agent_id, f"큐레이션 실패: {e}", level="error", task_id=task_id) await self.transition("idle", "오류") return {"ok": False, "message": str(e)} except Exception as e: update_task_status(task_id, "failed", result_data={"error": str(e)}) add_log(self.agent_id, f"큐레이션 예외: {e}", level="error", task_id=task_id) await self.transition("idle", "오류") return {"ok": False, "message": f"{type(e).__name__}: {e}"}