175 lines
7.9 KiB
Python
175 lines
7.9 KiB
Python
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 시그널 평가 (light/sim) 또는 deep_check (LLM 호출 후).
|
|
|
|
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
|
|
from ..db import add_log
|
|
|
|
if self.state not in ("idle", "reporting"):
|
|
return {"ok": False, "message": f"busy ({self.state})"}
|
|
|
|
try:
|
|
curate_result = None
|
|
current_draw_no = None
|
|
|
|
if source == "deep":
|
|
from ..curator.pipeline import curate_weekly
|
|
cw = await curate_weekly(source="signal_deep")
|
|
# curate_weekly returns {"ok", "draw_no", "confidence", "tokens", "payload"}
|
|
curate_result = {"confidence": cw.get("confidence")}
|
|
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,
|
|
)
|
|
add_log(
|
|
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:
|
|
"""일일 요약 — 지난 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(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}"}
|