Files
web-page-backend/agent-office/app/agents/lotto.py
2026-05-23 01:59:56 +09:00

215 lines
10 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 시그널 평가. 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통. task_id wrap."""
from ..db import (
create_task, update_task_status, add_log,
get_recent_lotto_signals, get_signals_history, get_baseline,
)
from ..notifiers.telegram_lotto import send_signal_summary
task_id = create_task("lotto", "daily_digest", {})
try:
sigs = get_recent_lotto_signals(hours=24, min_fire="normal")
total_24h = get_signals_history(days=1)
evaluated = len(total_24h)
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("lotto", f"weights_trend 계산 실패: {e}", level="warning", task_id=task_id)
digest = {
"evaluated": evaluated,
"fired": len(sigs),
"signals": sigs,
"weights_trend": trend,
}
await send_signal_summary(digest)
update_task_status(task_id, "succeeded", result_data={
"evaluated": evaluated,
"fired": len(sigs),
"signals_count": len(sigs),
})
add_log("lotto", f"daily_digest 발송: 평가 {evaluated} / 발화 {len(sigs)}", task_id=task_id)
return {"ok": True, **digest}
except Exception as e:
update_task_status(task_id, "failed", result_data={"error": str(e)})
add_log("lotto", f"daily_digest 예외: {e}", level="error", task_id=task_id)
return {"ok": False, "message": f"{type(e).__name__}: {e}"}
async def run_weekly_evolution_report(self) -> dict:
"""토 22:15 — lotto-lab evaluate-now 트리거 후 텔레그램 리포트. task_id wrap."""
from ..service_proxy import lotto_evolver_evaluate, lotto_evolver_status
from ..notifiers.telegram_lotto import send_evolution_report
from ..db import create_task, update_task_status, add_log
task_id = create_task("lotto", "weekly_evolution_report", {})
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)
winner = eval_result.get("winner") or {}
update_task_status(task_id, "succeeded", result_data={
"draw_no": eval_result.get("draw_no"),
"update_reason": eval_result.get("update_reason"),
"winner_day_of_week": winner.get("day_of_week"),
"winner_max_correct": winner.get("max_correct"),
})
add_log("lotto", f"weekly_evolution_report 발송: draw={eval_result.get('draw_no')} reason={eval_result.get('update_reason')}", task_id=task_id)
return {"ok": True, **eval_result}
except Exception as e:
update_task_status(task_id, "failed", result_data={"error": str(e)})
add_log("lotto", f"weekly_evolution_report 예외: {e}", level="error", task_id=task_id)
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}"}