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
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
|