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.

125 lines
5.6 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/tests/_fake_llm.py — 테스트 전용 결정적 LLM (구 HeuristicProvider 로직 이전, phase-16+)
# 프로덕션에서 휴리스틱 폴백을 제거했으므로, 테스트 결정성은 이 FakeLLM 을 get_provider 에
# 주입(conftest)해 확보한다. 실 Ollama 의존 없이 분류/분석/회신을 규칙으로 재현한다.
import re
from app.llm.provider import Classification, LLMProvider
RE_TIME = re.compile(
r"(\d{1,2}\s*시|\d{1,2}:\d{2}|오전|오후|"
r"월요일|화요일|수요일|목요일|금요일|토요일|일요일|"
r"월요|화요|수요|목요|금요|토요|일요|내일|모레|오늘)"
)
RE_ACTION = re.compile(r"(사기|구매|하기|맡기기|알아보기|보내기|작성|정리|예약|받|비교|확인|회신|준비)")
RE_WORK = re.compile(r"(미팅|회의|리포트|온보딩|OKR|리뷰|배포|스프린트|기획|디자인|개발|보고|발표)")
RE_LIFE = re.compile(r"(가족|여행|선물|구독|병원|생신|자전거|집|수영장|운동|독서|엄마|아빠|티켓)")
class FakeLLMProvider(LLMProvider):
name = "fake"
def health(self) -> dict:
return {"reachable": True, "provider": "fake", "model": "rules", "detail": "test fake"}
# ── generate_json: 스키마로 분기(메일 분석 / 회신 초안 / 기타) ──
def generate_json(self, prompt: str, schema: dict | None = None) -> dict:
props = (schema or {}).get("properties", {})
if "summary" in props and "priority" in props:
return self._analyze(prompt)
if set(props) == {"body"}:
return {"body": "안녕하세요,\n\n메일 확인했습니다. 빠르게 회신드리겠습니다.\n\n감사합니다."}
return {}
def _analyze(self, prompt: str) -> dict:
m = re.search(r"제목:\s*(.+)", prompt)
subject = (m.group(1).strip() if m else "")
tasks, events = [], []
if re.search(r"\d{1,2}시|\d{1,2}:\d{2}", prompt):
events.append(
{"title": subject[:24], "date": "", "day": "", "time": "", "dur": "", "place": ""}
)
if re.search(r"부탁|검토|회신|피드백|정리|확인", prompt):
tasks.append({"text": subject[:30], "project": "받은편지함", "due": "", "prio": "보통"})
prio = "높음" if ("[중요]" in subject or "마감" in prompt) else "보통"
return {
"summary": subject[:60],
"priority": prio,
"category": "메일",
"tasks": tasks,
"events": events,
"replies": [],
"file": None,
}
# ── classify_capture: 인박스 캡처 분류(구 휴리스틱 규칙 동일) ──
def classify_capture(self, raw: str, context: dict) -> Classification:
text = raw or ""
has_clock = bool(re.search(r"(\d{1,2}\s*시|\d{1,2}:\d{2})", text))
has_action = bool(RE_ACTION.search(text))
has_time = bool(RE_TIME.search(text))
if has_clock:
ctype = "event"
elif has_action:
ctype = "task"
elif has_time and not has_action:
ctype = "event"
else:
ctype = "idea"
if RE_WORK.search(text):
sphere = "work"
elif RE_LIFE.search(text):
sphere = "life"
else:
sphere = "work"
project_id, proj_label, tone = self._match_project(text, sphere, context)
due_text, when_text = self._time_hints(text)
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="fake",
)
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
if sphere == "life":
return "me", "개인 일상", "blue"
return None, "업무 받은 작업", "ink"
def _time_hints(self, text):
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 "아직 행동이 정해지지 않아 아이디어 보드에 보관했어요."