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