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.

142 lines
4.9 KiB
Python

# backend/app/routers/inbox.py
import uuid
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from ..db import get_session
from ..llm.provider import LLMProvider, get_provider
from ..models import InboxClassification, InboxItem, InboxStatus, Prio, Project, Task, TaskStatus
from ..routers.tasks import to_node
from ..schemas import (
CaptureRequest,
CaptureResponse,
ClassificationOut,
ConfirmResponse,
InboxItemOut,
ReclassifyRequest,
)
from ..services.classification import classify, persist_classification
router = APIRouter() # prefix 없음. main.py 에서 prefix="/api" 등록.
def latest_cls(s: Session, item_id: str) -> InboxClassification | None:
# id 가 TEXT PK(UUID)라 정렬 키로 부적합 → created_at 기준 최신.
rows = s.exec(
select(InboxClassification)
.where(InboxClassification.inbox_item_id == item_id)
.order_by(InboxClassification.created_at.desc())
).all()
return rows[0] if rows else None
def item_out(s: Session, item: InboxItem) -> InboxItemOut:
c = latest_cls(s, item.id)
cls = ClassificationOut.model_validate(c, from_attributes=True) if c else None
return InboxItemOut(
id=item.id, kind=item.kind, raw=item.raw, status=item.status,
created_at=item.created_at, materialized_task_id=item.materialized_task_id,
classification=cls,
)
@router.get("/inbox", response_model=list[InboxItemOut])
def list_inbox(s: Session = Depends(get_session)):
items = s.exec(select(InboxItem).order_by(InboxItem.created_at.desc())).all()
return [item_out(s, it) for it in items]
@router.post("/inbox/capture", response_model=CaptureResponse)
def capture(
body: CaptureRequest,
s: Session = Depends(get_session),
provider: LLMProvider = Depends(get_provider),
):
iid = "s-" + uuid.uuid4().hex[:8]
item = InboxItem(id=iid, kind=body.kind, raw=body.raw, status=InboxStatus.new)
s.add(item)
s.commit()
s.refresh(item)
c = classify(s, body.raw, provider) # 동기 분류 (LLM or 폴백)
row = persist_classification(s, iid, c)
item.status = InboxStatus.classified
s.add(item)
s.commit()
return CaptureResponse(
item=item_out(s, item),
classification=ClassificationOut.model_validate(row, from_attributes=True),
)
@router.post("/inbox/{iid}/reclassify", response_model=ClassificationOut)
def reclassify(
iid: str,
body: ReclassifyRequest,
s: Session = Depends(get_session),
provider: LLMProvider = Depends(get_provider),
):
item = s.get(InboxItem, iid)
if not item:
raise HTTPException(404, "inbox item not found")
c = classify(s, item.raw, provider, force_type=body.type.value if body.type else None)
row = persist_classification(s, iid, c) # 새 row → latest 가 최신
item.status = InboxStatus.classified
s.add(item)
s.commit()
return ClassificationOut.model_validate(row, from_attributes=True)
@router.post("/inbox/{iid}/confirm", response_model=ConfirmResponse)
def confirm(iid: str, s: Session = Depends(get_session)):
item = s.get(InboxItem, iid)
if not item:
raise HTTPException(404, "inbox item not found")
c = latest_cls(s, iid)
if not c:
raise HTTPException(400, "no classification to confirm")
created: Task | None = None
ctype = c.type.value if hasattr(c.type, "value") else c.type
csphere = c.sphere.value if hasattr(c.sphere, "value") else c.sphere
if ctype == "task":
# 프로젝트 결정: classification.project_id 있으면 그대로, 없으면 sphere 기본
pid = c.project_id
if not pid or not s.get(Project, pid):
pid = "me" if csphere == "life" else _fallback_work_project(s)
# confirm task 필드 정본(R10)
notes = "스마트 인박스에서 실체화된 작업이에요."
if c.extra:
notes += " " + c.extra # 예: 비행기 티켓 → "가격 추적 알림 켜둠"
tid = "t-" + uuid.uuid4().hex[:8]
created = Task(
id=tid, title=item.raw, project_id=pid, status=TaskStatus.todo,
assignee_id="jiwoo", prio=Prio.normal, due=None, notes=notes,
)
s.add(created)
s.commit()
s.refresh(created)
item.materialized_task_id = tid
item.status = InboxStatus.confirmed
s.add(item)
s.commit()
node = to_node(s, created, s.exec(select(Task)).all()) if created else None
return ConfirmResponse(item=item_out(s, item), task=node)
@router.post("/inbox/{iid}/dismiss", response_model=InboxItemOut)
def dismiss(iid: str, s: Session = Depends(get_session)):
item = s.get(InboxItem, iid)
if not item:
raise HTTPException(404, "inbox item not found")
item.status = InboxStatus.dismissed
s.add(item)
s.commit()
return item_out(s, item)
def _fallback_work_project(s: Session) -> str:
p = s.exec(select(Project).where(Project.folder_id == "work")).first()
return p.id if p else "me"