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.

226 lines
6.4 KiB
Python

# backend/app/routers/automation.py
import uuid
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from ..automation.event_bus import bus
from ..automation.events import AUTOMATION_MATCHED
from ..automation.nl_parser import EXAMPLES, parse_rule
from ..automation.suggester import scan
from ..db import get_session
from ..models import (
AutomationRule,
AutomationRunLog,
AutomationStats,
AutomationSuggestion,
)
from ..schemas import (
AutomationPageOut,
AutomationStatsOut,
FlowOut,
ParsePreviewOut,
ParseRequest,
RuleCreate,
RuleOut,
RulePatch,
RunLogOut,
SuggestionOut,
)
router = APIRouter() # prefix 없음. main.py 에서 prefix="/api" 등록.
def _rule_out(r: AutomationRule) -> RuleOut:
return RuleOut(
id=r.id,
name=r.name,
cat=r.cat,
trigger=r.trigger,
cond=r.cond,
action=r.action,
on=r.on,
last=r.last,
runs=r.runs,
fresh=r.fresh,
source=r.source,
)
def _sug_out(g: AutomationSuggestion) -> SuggestionOut:
return SuggestionOut(
id=g.id,
pattern=g.pattern,
offer=FlowOut(trigger=g.offer_trigger, cond=g.offer_cond, action=g.offer_action),
offer_name=g.offer_name,
offer_cat=g.offer_cat,
status=g.status,
)
@router.get("/automation", response_model=AutomationPageOut)
def get_page(s: Session = Depends(get_session)):
rules = s.exec(select(AutomationRule).order_by(AutomationRule.created_at)).all()
sugs = s.exec(select(AutomationSuggestion).where(AutomationSuggestion.status == "open")).all()
logs = s.exec(select(AutomationRunLog).order_by(AutomationRunLog.sort_order)).all()
st = s.exec(select(AutomationStats)).first()
active = sum(1 for r in rules if r.on)
stats = AutomationStatsOut(
active=active,
runs_week=(st.runs_week if st else 0),
saved=(st.saved if st else ""),
)
examples = [
ParsePreviewOut(
matched=True,
name=pr.name,
cat=pr.cat,
parse=FlowOut(trigger=pr.trigger, cond=pr.cond, action=pr.action),
model=pr.model,
confidence=pr.confidence,
)
for pr in EXAMPLES.values()
]
return AutomationPageOut(
stats=stats,
rules=[_rule_out(r) for r in rules],
suggests=[_sug_out(g) for g in sugs],
log=[
RunLogOut(id=log.id, time=log.time, rule=log.rule, text=log.text, undone=log.undone)
for log in logs
],
examples=examples,
)
@router.post("/automation/parse", response_model=ParsePreviewOut)
def parse(body: ParseRequest):
pr = parse_rule(body.text)
return ParsePreviewOut(
matched=pr.matched,
name=pr.name,
cat=pr.cat,
parse=FlowOut(trigger=pr.trigger, cond=pr.cond, action=pr.action),
model=pr.model,
confidence=pr.confidence,
)
@router.post("/automation/rules", response_model=RuleOut)
def create_rule(body: RuleCreate, s: Session = Depends(get_session)):
r = AutomationRule(
id="rule-" + uuid.uuid4().hex[:8],
name=body.name,
cat=body.cat,
trigger=body.trigger,
cond=body.cond,
action=body.action,
on=True,
last="방금 만듦",
runs=0,
fresh=True,
source=body.source or "user",
)
s.add(r)
s.commit()
s.refresh(r)
return _rule_out(r)
@router.patch("/automation/rules/{rid}", response_model=RuleOut)
def patch_rule(rid: str, body: RulePatch, s: Session = Depends(get_session)):
r = s.get(AutomationRule, rid)
if not r:
raise HTTPException(404, "rule not found")
for k, v in body.model_dump(exclude_unset=True).items():
setattr(r, k, v)
s.add(r)
s.commit()
s.refresh(r)
return _rule_out(r)
@router.post("/automation/rules/{rid}/toggle", response_model=RuleOut)
def toggle_rule(rid: str, s: Session = Depends(get_session)):
r = s.get(AutomationRule, rid)
if not r:
raise HTTPException(404, "rule not found")
r.on = not r.on
s.add(r)
s.commit()
s.refresh(r)
return _rule_out(r)
@router.delete("/automation/rules/{rid}")
def delete_rule(rid: str, s: Session = Depends(get_session)):
r = s.get(AutomationRule, rid)
if not r:
raise HTTPException(404, "rule not found")
s.delete(r)
s.commit()
return {"deleted": rid}
@router.post("/automation/suggestions/{sid}/accept", response_model=RuleOut)
def accept_suggestion(sid: str, s: Session = Depends(get_session)):
g = s.get(AutomationSuggestion, sid)
if not g:
raise HTTPException(404, "suggestion not found")
g.status = "accepted"
s.add(g)
r = AutomationRule(
id="rule-" + uuid.uuid4().hex[:8],
name=g.offer_name,
cat=g.offer_cat,
trigger=g.offer_trigger,
cond=g.offer_cond,
action=g.offer_action,
on=True,
last="방금 만듦",
runs=0,
fresh=True,
source="suggestion",
)
s.add(r)
s.commit()
s.refresh(r)
return _rule_out(r)
@router.post("/automation/suggestions/{sid}/dismiss")
def dismiss_suggestion(sid: str, s: Session = Depends(get_session)):
g = s.get(AutomationSuggestion, sid)
if not g:
raise HTTPException(404, "suggestion not found")
g.status = "dismissed"
s.add(g)
s.commit()
return {"dismissed": sid}
@router.post("/automation/suggest/scan")
def suggest_scan(s: Session = Depends(get_session)):
created = scan(s, bus) # 생성 제안마다 automation.suggested 발행
return {"created": [g.id for g in created]}
@router.get("/automation/log", response_model=list[RunLogOut])
def get_log(s: Session = Depends(get_session)):
logs = s.exec(select(AutomationRunLog).order_by(AutomationRunLog.sort_order)).all()
return [
RunLogOut(id=log.id, time=log.time, rule=log.rule, text=log.text, undone=log.undone)
for log in logs
]
@router.post("/automation/trigger")
def manual_trigger(trigger_key: str, s: Session = Depends(get_session)):
"""데모/테스트용 수동 트리거 — AUTOMATION_MATCHED 발행(스케줄러는 phase-14).
evaluator 가 구독해 매칭 규칙을 승인 큐에 enqueue 한다."""
ev = bus.publish(
AUTOMATION_MATCHED,
{"trigger_key": trigger_key, "ctx": {"title": f"{trigger_key} 규칙 실행"}},
)
return {"published": ev.type, "trigger_key": trigger_key}