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.

107 lines
4.5 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 "아직 행동이 정해지지 않아 아이디어 보드에 보관했어요."