# 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}