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.

114 lines
3.6 KiB
Python

# backend/app/approvals/service.py
# 큐 분기(derive_queue = 원본 approve.jsx initStatus) + 상태 전이.
from __future__ import annotations
from datetime import UTC, datetime
from sqlmodel import Session, select
from ..automation.event_bus import bus
from ..automation.events import APPROVAL_EXECUTED, APPROVAL_UNDONE
from ..models import (
Approval,
ApprovalLog,
ApprovalStatus,
AutonomyLevel,
AutonomySetting,
RiskLevel,
)
_LEVELS = (
AutonomyLevel.approval_first.value,
AutonomyLevel.mixed.value,
AutonomyLevel.full_auto.value,
)
def get_autonomy(s: Session) -> str:
row = s.get(AutonomySetting, "default")
if not row:
row = AutonomySetting(id="default", level=AutonomyLevel.mixed.value)
s.add(row)
s.commit()
s.refresh(row)
return row.level
def set_autonomy(s: Session, level: str) -> str:
if level not in _LEVELS:
raise ValueError("invalid autonomy level")
row = s.get(AutonomySetting, "default") or AutonomySetting(id="default")
row.level = level
row.updated_at = datetime.now(UTC)
s.add(row)
s.commit()
return level
def derive_status(level: str, risk: str) -> str:
"""원본 approve.jsx initStatus(autonomy) 이식.
full_auto: 전부 executed. mixed: low=executed, high=pending. approval_first: 전부 pending."""
if level == AutonomyLevel.full_auto.value:
return ApprovalStatus.executed.value
if level == AutonomyLevel.mixed.value:
return (
ApprovalStatus.executed.value
if risk == RiskLevel.low.value
else ApprovalStatus.pending.value
)
return ApprovalStatus.pending.value # approval_first
def derive_queue(s: Session, user_id: str | None = None) -> dict:
"""자율성 레벨로 모든 approval 의 표시 상태를 파생.
단, 사용자가 명시적으로 undone 한 것은 pending, 엔진이 실제 실행한 건은 executed 로 고정.
user_id 가 주어지면 소유자 스코프(phase-15)."""
level = get_autonomy(s)
stmt = select(Approval).order_by(Approval.sort_order)
if user_id is not None:
stmt = stmt.where(Approval.user_id == user_id)
rows = s.exec(stmt).all()
pending, done = [], []
for a in rows:
if a.status == ApprovalStatus.undone.value: # 사용자가 되돌림 → 대기로
eff = ApprovalStatus.pending.value
elif a.status == ApprovalStatus.executed.value and a.executed_at:
eff = ApprovalStatus.executed.value # 사용자/엔진이 실제 실행한 건은 고정
else:
eff = derive_status(level, a.risk) # 시드/표시용은 레벨로 파생
(done if eff == ApprovalStatus.executed.value else pending).append(a)
return {"level": level, "pending": pending, "done": done}
def approve(s: Session, aid: str) -> Approval:
a = s.get(Approval, aid)
a.status = ApprovalStatus.executed.value
a.executed_at = datetime.now(UTC)
s.add(a)
s.commit()
s.refresh(a)
s.add(ApprovalLog(id="alog-" + aid + "-x", time="방금", text=a.title, approval_id=aid))
s.commit()
bus.publish(APPROVAL_EXECUTED, {"approval_id": aid, "risk": a.risk, "title": a.title})
return a
def undo(s: Session, aid: str) -> Approval:
a = s.get(Approval, aid)
a.status = ApprovalStatus.undone.value
a.undone_at = datetime.now(UTC)
s.add(a)
s.commit()
s.refresh(a)
bus.publish(APPROVAL_UNDONE, {"approval_id": aid, "title": a.title})
return a
def approve_all(s: Session) -> int:
q = derive_queue(s)
n = 0
for a in q["pending"]:
approve(s, a.id)
n += 1
return n