From c5303151c0f43c151c89273e678e600a7601f3ae Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 23 May 2026 02:06:30 +0900 Subject: [PATCH] =?UTF-8?q?feat(lotto-agent):=20sync=5Fevolver=5Factivity?= =?UTF-8?q?=20=EB=A7=A4=EC=9D=BC=2009:30=20cron=20+=20=EB=A9=B1=EB=93=B1?= =?UTF-8?q?=20=EA=B0=80=EB=93=9C=20+=203=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LottoAgent.sync_evolver_activity(): lotto-lab evolver status polling → agent_office.db task+log 미러링 - UTC 날짜 기준 멱등 guard (get_tasks_by_agent_date_kind 활용) - 일요일(dow=6) → 5 clamp (lotto-lab trials는 0~5) - 월요일 6-trial 완성 시 evolver_generate task 추가 생성 - scheduler.py: lotto_evolver_activity_sync cron 09:30 등록 - tests: creates_apply_task / idempotent / no_picks_no_task 3종 Co-Authored-By: Claude Sonnet 4.6 --- agent-office/app/agents/lotto.py | 53 ++++++++ agent-office/app/scheduler.py | 15 ++- .../tests/test_sync_evolver_activity.py | 121 ++++++++++++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 agent-office/tests/test_sync_evolver_activity.py diff --git a/agent-office/app/agents/lotto.py b/agent-office/app/agents/lotto.py index ad752a3..ae791e8 100644 --- a/agent-office/app/agents/lotto.py +++ b/agent-office/app/agents/lotto.py @@ -182,6 +182,59 @@ class LottoAgent(BaseAgent): 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 sync_evolver_activity(self) -> dict: + """매일 09:30 — lotto-lab evolver 상태 polling → agent_office.db에 task+log 거울. 멱등.""" + from datetime import datetime, timezone, timedelta + from ..service_proxy import lotto_evolver_status + from ..db import ( + create_task, update_task_status, add_log, + get_tasks_by_agent_date_kind, + ) + + KST = timezone(timedelta(hours=9)) + today_kst = datetime.now(KST).date() + # created_at은 UTC로 저장되므로 idempotency guard는 UTC 날짜 기준 + today_utc_iso = datetime.now(timezone.utc).date().isoformat() + dow = today_kst.weekday() + if dow == 6: + dow = 5 + + try: + status = await lotto_evolver_status() + except Exception as e: + add_log("lotto", f"sync_evolver_activity: lotto-lab status fetch 실패: {e}", level="warning") + return {"ok": False, "reason": "status_fetch_failed", "error": str(e)} + + results = {"created": []} + + today_trial = next((t for t in status.get("trials", []) if t.get("day_of_week") == dow), None) + if today_trial and today_trial.get("picks"): + if not get_tasks_by_agent_date_kind("lotto", today_utc_iso, "evolver_apply"): + tid = create_task("lotto", "evolver_apply", { + "date": today_utc_iso, + "trial_id": today_trial["id"], + "day_of_week": dow, + "weight": today_trial["weight"], + }) + update_task_status(tid, "succeeded", result_data={ + "n_picks": len(today_trial["picks"]), + "meta_scores": [p.get("meta_score") for p in today_trial["picks"]], + }) + add_log("lotto", f"evolver_apply: 오늘({dow}) W로 {len(today_trial['picks'])}세트 추출", task_id=tid) + results["created"].append("evolver_apply") + + if today_kst.weekday() == 0 and len(status.get("trials", [])) == 6: + if not get_tasks_by_agent_date_kind("lotto", today_utc_iso, "evolver_generate"): + tid = create_task("lotto", "evolver_generate", {"week_start": status.get("week_start")}) + update_task_status(tid, "succeeded", result_data={ + "trials_count": 6, + "candidates_per_source": {"perturb": 4, "dirichlet": 2}, + }) + add_log("lotto", f"evolver_generate: {status.get('week_start')} 주의 6 trials 생성", task_id=tid) + results["created"].append("evolver_generate") + + return {"ok": True, **results} + 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) diff --git a/agent-office/app/scheduler.py b/agent-office/app/scheduler.py index aba3776..deb1b6e 100644 --- a/agent-office/app/scheduler.py +++ b/agent-office/app/scheduler.py @@ -61,6 +61,11 @@ async def _run_lotto_weekly_evolution_report(): if agent: await agent.run_weekly_evolution_report() +async def _run_lotto_sync_evolver_activity(): + agent = AGENT_REGISTRY.get("lotto") + if agent: + await agent.sync_evolver_activity() + async def _run_youtube_research(): agent = AGENT_REGISTRY.get("youtube") if agent: @@ -95,14 +100,20 @@ def init_scheduler(): id="stock_ai_news_sentiment", ) scheduler.add_job(_run_insta_schedule, "cron", hour=9, minute=30, id="insta_pipeline") - # 09:00 cron 스태거링 — Celeron 2C/2.0GHz에서 동시 실행 시 CPU 폭주 (CHECK_POINT FU-A) - scheduler.add_job(_run_insta_trends_collect, "cron", hour=9, minute=0, id="insta_trends_collect") + # 외부 트렌드 수집은 장 마감 후 16:40 — 9시 주식 활발 시간대 NAS 자원 회피. + # screener(16:30)와 10분 스태거: Celeron 2C/2.0GHz 동시 실행 시 CPU 폭주 방지 (CHECK_POINT FU-A) + scheduler.add_job(_run_insta_trends_collect, "cron", hour=16, minute=40, id="insta_trends_collect") scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=9, minute=5, id="lotto_curate") scheduler.add_job(_run_lotto_light_check, "cron", hour=9, minute=15, id="lotto_light_check") scheduler.add_job(_run_lotto_sim_check, "cron", minute=15, hour="0,4,8,12,16,20", id="lotto_sim_check") scheduler.add_job(_run_lotto_deep_check, "cron", day_of_week="sun,wed", hour=21, minute=15, id="lotto_deep_check") scheduler.add_job(_run_lotto_daily_digest, "cron", hour=9, minute=25, id="lotto_digest") scheduler.add_job(_run_lotto_weekly_evolution_report, "cron", day_of_week="sat", hour=22, minute=15, id="lotto_evolution_weekly") + scheduler.add_job( + _run_lotto_sync_evolver_activity, + "cron", hour=9, minute=30, + id="lotto_evolver_activity_sync", + ) scheduler.add_job(_run_youtube_research, "cron", hour=9, minute=10, id="youtube_research") scheduler.add_job(_send_youtube_weekly_report, "cron", day_of_week="mon", hour=8, minute=0, id="youtube_weekly_report") scheduler.add_job(_poll_pipelines, "interval", seconds=30, id="pipeline_poll") diff --git a/agent-office/tests/test_sync_evolver_activity.py b/agent-office/tests/test_sync_evolver_activity.py new file mode 100644 index 0000000..88b39ad --- /dev/null +++ b/agent-office/tests/test_sync_evolver_activity.py @@ -0,0 +1,121 @@ +# agent-office/tests/test_sync_evolver_activity.py +import os +import sys +import tempfile +import gc +from datetime import datetime, timezone, timedelta + +_fd, _TMP = tempfile.mkstemp(suffix=".db") +os.close(_fd) +os.unlink(_TMP) +os.environ["AGENT_OFFICE_DB_PATH"] = _TMP + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import pytest +from app import db +db.DB_PATH = _TMP + + +@pytest.fixture(autouse=True) +def fresh_db(): + gc.collect() + if os.path.exists(_TMP): + os.remove(_TMP) + db.init_db() + yield + gc.collect() + if os.path.exists(_TMP): + try: + os.remove(_TMP) + except PermissionError: + pass + + +def _today_dow_clamped(): + """오늘의 weekday() (일요일=6은 5로 clamp).""" + KST = timezone(timedelta(hours=9)) + dow = datetime.now(KST).weekday() + return 5 if dow == 6 else dow + + +def _fake_status_with_picks(dow_with_picks): + async def fake(): + return { + "week_start": "2026-05-18", + "current_base": [0.2] * 5, + "trials": [ + { + "id": 100 + i, + "day_of_week": i, + "weight": [0.2] * 5, + "source": "perturb", + "picks": ([ + {"id": j, "numbers": [1,2,3,4,5,6], "meta_score": 0.5} + for j in range(5) + ] if i == dow_with_picks else []), + } + for i in range(6) + ], + } + return fake + + +@pytest.mark.asyncio +async def test_sync_evolver_activity_creates_apply_task(monkeypatch): + """오늘 trial에 picks가 있으면 evolver_apply task 1개 생성.""" + from app.agents.lotto import LottoAgent + from app import service_proxy + + dow = _today_dow_clamped() + monkeypatch.setattr(service_proxy, "lotto_evolver_status", _fake_status_with_picks(dow)) + + agent = LottoAgent() + await agent.sync_evolver_activity() + + apply_tasks = db.get_agent_tasks("lotto", task_type="evolver_apply", days=1) + assert len(apply_tasks) == 1 + assert apply_tasks[0]["result_data"]["n_picks"] == 5 + assert apply_tasks[0]["input_data"]["day_of_week"] == dow + + +@pytest.mark.asyncio +async def test_sync_evolver_activity_idempotent(monkeypatch): + """같은 날 두 번 호출해도 task는 1개만 (멱등).""" + from app.agents.lotto import LottoAgent + from app import service_proxy + + dow = _today_dow_clamped() + monkeypatch.setattr(service_proxy, "lotto_evolver_status", _fake_status_with_picks(dow)) + + agent = LottoAgent() + await agent.sync_evolver_activity() + await agent.sync_evolver_activity() + + apply_tasks = db.get_agent_tasks("lotto", task_type="evolver_apply", days=1) + assert len(apply_tasks) == 1 + + +@pytest.mark.asyncio +async def test_sync_evolver_activity_no_picks_no_task(monkeypatch): + """오늘 trial에 picks가 없으면 task 생성하지 않음.""" + from app.agents.lotto import LottoAgent + from app import service_proxy + + async def fake_status(): + return { + "week_start": "2026-05-18", + "current_base": [0.2] * 5, + "trials": [ + {"id": 100 + i, "day_of_week": i, "weight": [0.2]*5, + "source": "perturb", "picks": []} + for i in range(6) + ], + } + monkeypatch.setattr(service_proxy, "lotto_evolver_status", fake_status) + + agent = LottoAgent() + await agent.sync_evolver_activity() + + apply_tasks = db.get_agent_tasks("lotto", task_type="evolver_apply", days=1) + assert len(apply_tasks) == 0