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.

146 lines
4.5 KiB
Python

# backend/app/routers/worker.py — 능동 워커 트리거 + 능동 카드 + 주간 리뷰 API (phase-14)
import json
import uuid
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from ..db import get_session
from ..models import (
AutomationRule,
FocusBlock,
ProactiveCard,
Project,
Task,
TaskStatus,
)
from ..schemas import (
ProactiveAcceptOut,
ProactiveCardOut,
WeeklyReviewOut,
WorkerRunOut,
)
from ..worker.jobs.weekly_review import run_weekly_review
from ..worker.triggers import JOBS
router = APIRouter()
def _pc_out(c: ProactiveCard) -> ProactiveCardOut:
return ProactiveCardOut(
id=c.id,
kind=c.kind,
icon=c.icon,
tone=c.tone,
title=c.title,
why=c.why,
cta=c.cta,
action_kind=c.action_kind,
detected_at=c.detected_at,
status=c.status,
)
@router.post("/worker/run/{job}", response_model=WorkerRunOut)
def run_job(job: str, payload: dict | None = None, s: Session = Depends(get_session)):
if job not in JOBS:
raise HTTPException(404, f"unknown job: {job}")
result = JOBS[job](s, payload or {})
return WorkerRunOut(job=job, ok=True, detail=json.dumps(result, ensure_ascii=False))
@router.get("/proactive", response_model=list[ProactiveCardOut])
def list_proactive(s: Session = Depends(get_session)):
rows = s.exec(select(ProactiveCard).where(ProactiveCard.status == "active")).all()
return [_pc_out(c) for c in rows]
@router.post("/proactive/{pid}/accept", response_model=ProactiveAcceptOut)
def accept_proactive(pid: str, s: Session = Depends(get_session)):
c = s.get(ProactiveCard, pid)
if not c:
raise HTTPException(404, "proactive card not found")
created_id, message = _materialize(s, c)
c.status = "accepted"
s.add(c)
s.commit()
return ProactiveAcceptOut(
id=c.id, action_kind=c.action_kind or "none", created_id=created_id, message=message
)
@router.post("/proactive/{pid}/dismiss")
def dismiss_proactive(pid: str, s: Session = Depends(get_session)):
c = s.get(ProactiveCard, pid)
if not c:
raise HTTPException(404, "proactive card not found")
c.status = "dismissed"
s.add(c)
s.commit()
return {"ok": True}
@router.get("/weekly-review", response_model=WeeklyReviewOut)
def weekly_review(week: str = "6/7~6/13", s: Session = Depends(get_session)):
wr = run_weekly_review(s, week_label=week)
return WeeklyReviewOut(
id=wr.id,
week_label=wr.week_label,
deep_work_h=wr.deep_work_h,
meeting_h=wr.meeting_h,
done_count=wr.done_count,
auto_count=wr.auto_count,
saved_minutes=wr.saved_minutes,
patterns=json.loads(wr.patterns_json or "[]"),
note=wr.note,
)
def _materialize(s: Session, c: ProactiveCard) -> tuple[str | None, str]:
"""action_kind → focus_block/task/automation 실체화. (id, 메시지) 반환."""
payload = json.loads(c.action_payload) if c.action_payload else {}
kind = c.action_kind
if kind == "focus_block":
fb = FocusBlock(
id="fb-" + uuid.uuid4().hex[:8],
day=int(payload.get("day", 0)),
start=payload.get("start", ""),
end=payload.get("end", ""),
title=payload.get("title", "보호 블록"),
type=payload.get("type", "light"),
auto=True,
)
s.add(fb)
s.commit()
return fb.id, "집중 블록을 잡아뒀어요"
if kind == "task":
pid = payload.get("project_id", "me")
if not s.get(Project, pid):
first = s.exec(select(Project)).first()
pid = first.id if first else pid
t = Task(
id="kx-" + uuid.uuid4().hex[:8],
project_id=pid,
title=payload.get("title", "확인할 일"),
status=TaskStatus.todo,
)
s.add(t)
s.commit()
return t.id, "작업으로 추가했어요"
if kind == "automation":
r = AutomationRule(
id="rule-" + uuid.uuid4().hex[:8],
name=payload.get("name", "새 규칙"),
cat=payload.get("cat", "focus"),
trigger=payload.get("trigger", ""),
cond=payload.get("cond"),
action=payload.get("action", ""),
source="suggestion",
fresh=True,
last="방금 만듦",
)
s.add(r)
s.commit()
return r.id, "자동화 규칙을 만들었어요"
return None, "확인했어요"