You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

89 lines
3.1 KiB
Python

# backend/app/worker/jobs/proactive.py — 능동 감지 잡 → ProactiveCard (phase-14)
import json
from sqlmodel import Session, select
from ...automation.event_bus import bus
from ...config import get_settings
from ...models import CalEvent, FinanceCategory, ProactiveCard, ProactiveKind
def _dumps(d: dict) -> str:
return json.dumps(d, ensure_ascii=False)
def _tomorrow() -> int:
return get_settings().risk_today + 1
def run_proactive(session: Session) -> list[ProactiveCard]:
"""작업·일정·금융을 훑어 능동 카드를 만든다(임계값 기반 → 결정적).
같은 시드에서 항상 같은 카드(id 고정 upsert)."""
cards: list[ProactiveCard] = []
cards += _detect_meeting_streak(session)
cards += _detect_pricey_bill(session)
out: list[ProactiveCard] = []
for c in cards:
existing = session.get(ProactiveCard, c.id)
if existing:
if existing.status == "dismissed":
continue
out.append(existing)
continue
session.add(c)
out.append(c)
bus.publish("proactive.detected", {"id": c.id, "kind": c.kind})
session.commit()
return out
def _detect_meeting_streak(s: Session) -> list[ProactiveCard]:
"""내일 회의가 3건 이상이면 점심 보호 카드."""
day = _tomorrow()
events = s.exec(select(CalEvent).where(CalEvent.day == day)).all()
if len(events) >= 3:
return [
ProactiveCard(
id="pc-streak",
kind=ProactiveKind.schedule.value,
icon="cal",
tone="violet",
title="내일 미팅 3연속 — 점심 비워뒀어요",
why="연속 일정 사이 12시 공백을 보호 블록으로 잡았어요",
cta="그대로 둘게요",
action_kind="focus_block",
action_payload=_dumps(
{
"day": day,
"start": "12:00",
"end": "13:00",
"type": "light",
"title": "점심·재충전 보호",
}
),
detected_at="방금",
)
]
return []
def _detect_pricey_bill(s: Session) -> list[ProactiveCard]:
"""예산 초과 카테고리가 있으면 사유 확인 카드."""
out: list[ProactiveCard] = []
for cat in s.exec(select(FinanceCategory).where(FinanceCategory.over.is_(True))).all():
out.append(
ProactiveCard(
id=f"pc-bill-{cat.id}",
kind=ProactiveKind.finance.value,
icon="wallet",
tone="amber",
title=f"{cat.name} 지출이 평소보다 많아요",
why=f"이번 달 {cat.amt:,}원 · 예산을 넘었어요 — 사유 확인 권장",
cta="자세히",
action_kind="task",
action_payload=_dumps({"title": f"{cat.name} 지출 사유 확인", "project_id": "me"}),
detected_at="방금",
)
)
return out