|
|
# backend/app/llm/heuristic.py
|
|
|
import re
|
|
|
|
|
|
from .provider import Classification, LLMProvider
|
|
|
|
|
|
# 시간 표현 → event (요일/시각/날짜)
|
|
|
RE_TIME = re.compile(
|
|
|
r"(\d{1,2}\s*시|\d{1,2}:\d{2}|오전|오후|"
|
|
|
r"월요일|화요일|수요일|목요일|금요일|토요일|일요일|"
|
|
|
r"월요|화요|수요|목요|금요|토요|일요|내일|모레|오늘)"
|
|
|
)
|
|
|
# 행동 동사 + 기한 → task
|
|
|
RE_ACTION = re.compile(r"(사기|구매|하기|맡기기|알아보기|보내기|작성|정리|예약|받|비교|확인|회신|준비)") # noqa: E501
|
|
|
# work / life 키워드
|
|
|
RE_WORK = re.compile(r"(미팅|회의|리포트|온보딩|OKR|리뷰|배포|스프린트|기획|디자인|개발|보고|발표)")
|
|
|
RE_LIFE = re.compile(r"(가족|여행|선물|구독|병원|생신|자전거|집|수영장|운동|독서|엄마|아빠|티켓)")
|
|
|
|
|
|
WEEKDAY = {"월": "월", "화": "화", "수": "수", "목": "목", "금": "금", "토": "토", "일": "일"}
|
|
|
|
|
|
|
|
|
class HeuristicProvider(LLMProvider):
|
|
|
name = "heuristic"
|
|
|
|
|
|
def health(self) -> dict:
|
|
|
return {
|
|
|
"reachable": True,
|
|
|
"provider": "heuristic",
|
|
|
"model": "rules",
|
|
|
"detail": "rule-based fallback",
|
|
|
}
|
|
|
|
|
|
def generate_json(self, prompt: str, schema: dict | None = None) -> dict:
|
|
|
return {} # 폴백 시 scaffold 등은 호출측이 템플릿 사용
|
|
|
|
|
|
def classify_capture(self, raw: str, context: dict) -> Classification:
|
|
|
text = raw or ""
|
|
|
# 1) type: 시간명시 → event, 행동+기한 → task, 막연 → idea
|
|
|
has_time = bool(RE_TIME.search(text))
|
|
|
has_clock = bool(re.search(r"(\d{1,2}\s*시|\d{1,2}:\d{2})", text))
|
|
|
has_action = bool(RE_ACTION.search(text))
|
|
|
if has_clock:
|
|
|
ctype = "event"
|
|
|
elif has_action:
|
|
|
ctype = "task"
|
|
|
elif has_time and not has_action:
|
|
|
ctype = "event"
|
|
|
else:
|
|
|
ctype = "idea"
|
|
|
|
|
|
# 2) sphere
|
|
|
if RE_WORK.search(text):
|
|
|
sphere = "work"
|
|
|
elif RE_LIFE.search(text):
|
|
|
sphere = "life"
|
|
|
else:
|
|
|
sphere = "work" # 기본 업무
|
|
|
|
|
|
# 3) project 매칭 (context["projects"]: [{id,name,folder_id}])
|
|
|
project_id, proj_label, tone = self._match_project(text, sphere, context)
|
|
|
|
|
|
# 4) due/when/extra/reason
|
|
|
due_text, when_text = self._time_hints(text, ctype)
|
|
|
extra = "가격 추적 알림 켜둠" if re.search(r"(티켓|구매|가격)", text) else ""
|
|
|
reason = self._reason(ctype, sphere, proj_label)
|
|
|
|
|
|
conf = 0.85 if has_clock or has_action else 0.6
|
|
|
return Classification(
|
|
|
type=ctype, sphere=sphere, project_id=project_id, proj_label=proj_label,
|
|
|
tone=tone, due_text=due_text, when_text=when_text, extra=extra,
|
|
|
reason=reason, confidence=conf, model="heuristic",
|
|
|
)
|
|
|
|
|
|
def _match_project(self, text, sphere, context):
|
|
|
# 키워드 → 알려진 프로젝트
|
|
|
rules = [
|
|
|
(r"(여행|티켓|비행기|한국)", "life-trip", "개인 › 여행 — 한국", "coral"),
|
|
|
(r"(엄마|아빠|가족|생신|부모)", "life-fam", "가족", "green"),
|
|
|
(r"(온보딩)", "onb", "온보딩 리디자인 · 아이디어 보드", "violet"),
|
|
|
(r"(자전거|수리|병원|예약)", None, "개인 캘린더", "blue"),
|
|
|
]
|
|
|
for pat, pid, label, tone in rules:
|
|
|
if re.search(pat, text):
|
|
|
return pid, label, tone
|
|
|
# 없으면 sphere 기본
|
|
|
if sphere == "life":
|
|
|
return "me", "개인 › 일상", "blue"
|
|
|
return None, "업무 › 받은 작업", "ink"
|
|
|
|
|
|
def _time_hints(self, text, ctype):
|
|
|
m = re.search(r"(월|화|수|목|금|토|일)요일?\s*(\d{1,2})\s*시", text)
|
|
|
if m:
|
|
|
return "", f"{m.group(1)} {int(m.group(2)):02d}:00"
|
|
|
if "다음 주" in text:
|
|
|
return "출발 전 · ~6/14", "오늘 21:00 빈 시간 추천"
|
|
|
return "", ""
|
|
|
|
|
|
def _reason(self, ctype, sphere, proj_label):
|
|
|
sph = "업무" if sphere == "work" else "개인"
|
|
|
if ctype == "event":
|
|
|
return "시간이 정해진 일은 작업이 아니라 일정으로 바로 등록해요."
|
|
|
if ctype == "task":
|
|
|
return (
|
|
|
f"행동이 있으니 '작업' 맞아요. {sph} 트리의 '{proj_label}'에 넣었어요 — "
|
|
|
"따로 섹션이 생기는 게 아니라 다른 작업과 똑같이 보여요."
|
|
|
)
|
|
|
return "아직 행동이 정해지지 않아 아이디어 보드에 보관했어요."
|