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.

128 lines
4.4 KiB
Python

# backend/app/automation/evaluator.py
# 이벤트 → 규칙 매칭 → 동작/승인 enqueue. automation.matched → approval enqueue 연합 고리.
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from sqlmodel import Session, select
from ..models import (
Approval,
ApprovalLog,
ApprovalSource,
ApprovalStatus,
AutomationRule,
AutomationRunLog,
AutonomyLevel,
AutonomySetting,
RiskLevel,
)
from .event_bus import Event, EventBus
from .events import APPROVAL_ENQUEUED, APPROVAL_EXECUTED, AUTOMATION_MATCHED
# cat → 결재 카드 기본 tone/icon(원본 매핑과 일관)
CAT_TONE = {"mail": "blue", "cal": "violet", "focus": "coral", "life": "green"}
CAT_ICON = {"mail": "mail", "cal": "cal", "focus": "zap", "life": "wallet"}
# 어떤 동작이 high-risk 인가 — 보내기/결제/삭제/전달 키워드
HIGH_RISK_RE = ("보내", "결제", "삭제", "전달", "발송", "일시정지", "송금")
def _risk_of(action: str) -> str:
return RiskLevel.high.value if any(k in action for k in HIGH_RISK_RE) else RiskLevel.low.value
def _autonomy(s: Session) -> str:
row = s.get(AutonomySetting, "default")
return row.level if row else AutonomyLevel.mixed.value
def match_rules(s: Session, trigger_key: str) -> list[AutomationRule]:
"""trigger_key(예: 'mail.newsletter')와 켜진 규칙을 매칭.
데모는 cat 기반 단순 매칭 + trigger 문자열 포함. 실연동(phase-13)이 정교화."""
rules = s.exec(select(AutomationRule).where(AutomationRule.on == True)).all() # noqa: E712
head = trigger_key.split(".")[0]
tail = trigger_key.split(".")[-1]
return [r for r in rules if head in (r.cat, "") or tail in r.trigger]
def enqueue_from_rule(s: Session, rule: AutomationRule, ctx: dict, bus: EventBus) -> Approval:
"""규칙 동작을 승인 큐에 넣는다. low+mixed이상=자동 실행(executed), 그 외=pending."""
risk = _risk_of(rule.action)
level = _autonomy(s)
auto_run = risk == RiskLevel.low.value and level in (
AutonomyLevel.mixed.value,
AutonomyLevel.full_auto.value,
)
ap = Approval(
id="ap-" + uuid.uuid4().hex[:8],
icon=CAT_ICON.get(rule.cat, "spark"),
tone=CAT_TONE.get(rule.cat, "blue"),
risk=risk,
time=("자동 실행됨" if auto_run else "확인 필요"),
title=ctx.get("title", rule.action),
detail=ctx.get("detail", f"{rule.name} · {rule.trigger}"),
cta=ctx.get("cta", "" if risk == RiskLevel.low.value else "실행"),
alt=ctx.get("alt", "" if risk == RiskLevel.low.value else "나중에"),
undo_label=(ctx.get("undo_label", "되돌리기") if risk == RiskLevel.low.value else ""),
status=(ApprovalStatus.executed.value if auto_run else ApprovalStatus.pending.value),
source=ApprovalSource.automation.value,
rule_id=rule.id,
executed_at=(datetime.now(UTC) if auto_run else None),
)
s.add(ap)
s.add(
AutomationRunLog(
id="arun-" + uuid.uuid4().hex[:8],
time="방금",
rule_id=rule.id,
rule=rule.name,
text=ctx.get("text", rule.action),
)
)
if auto_run:
s.add(
ApprovalLog(
id="alog-" + uuid.uuid4().hex[:8],
time="방금",
text=ctx.get("text", rule.action),
approval_id=ap.id,
)
)
rule.runs += 1
rule.last = "방금"
s.add(rule)
s.commit()
s.refresh(ap)
# 결재 enqueue 정본 이벤트명 = approval.enqueued
bus.publish(
APPROVAL_ENQUEUED,
{
"approval_id": ap.id,
"rule_id": rule.id,
"risk": risk,
"title": ap.title,
"auto_run": auto_run,
},
)
if auto_run:
bus.publish(
APPROVAL_EXECUTED,
{"approval_id": ap.id, "rule_id": rule.id, "risk": risk, "title": ap.title},
)
return ap
def register(bus: EventBus, session_factory) -> None:
"""버스에 evaluator 핸들러 등록. session_factory()는 Session 컨텍스트를 yield."""
def on_matched(ev: Event) -> None:
trigger_key = ev.payload.get("trigger_key", "")
ctx = ev.payload.get("ctx", {})
with session_factory() as s:
for rule in match_rules(s, trigger_key):
enqueue_from_rule(s, rule, ctx, bus)
bus.subscribe(AUTOMATION_MATCHED, on_matched)