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