Files
leocrm/app/workflows/decision_guard.py
T

148 lines
4.3 KiB
Python
Raw Normal View History

"""Automated-Decision Guard (G-HUMAN-DEC).
For AI use cases configured as requiring human review, workflow/agent
results must not automatically trigger defined person-/risk-relevant
external effects before the required human review/approval policy is
fulfilled.
This guard checks the AIUseCaseMetadata of an agent or workflow step
and blocks automatic execution of high-risk actions unless approval
has been granted.
No blanket enforcement for normal CRM automation — only applies to
explicitly configured AI use cases.
"""
from __future__ import annotations
import logging
import uuid
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
# Risk levels for automated decisions
RISK_LEVELS = {
"none": 0,
"low": 1,
"medium": 2,
"high": 3,
"critical": 4,
}
# Actions that require human review when risk_level >= medium
HIGH_RISK_ACTIONS = {
"send_email",
"send_external_message",
"delete_entity",
"modify_permissions",
"execute_payment",
"publish_content",
"modify_contract",
"data_export",
}
def requires_human_review(
ai_use_case_metadata: dict[str, Any] | None,
action: str,
) -> bool:
"""Check whether an action requires human review based on AI use case config.
Args:
ai_use_case_metadata: The ai_use_case_metadata from AgentDefinition
or workflow step config. May contain:
- ``risk_level``: none|low|medium|high|critical
- ``requires_approval``: bool
- ``auto_execute``: bool (if False, always requires review)
- ``reviewed_actions``: list of actions that need review
action: The action being performed (e.g. "send_email", "delete_entity").
Returns:
True if human review is required before the action can execute.
"""
if not ai_use_case_metadata:
return False # No AI use case config → no guard
# If auto_execute is explicitly False, always require review
if ai_use_case_metadata.get("auto_execute") is False:
return True
# If requires_approval is True, always require review
if ai_use_case_metadata.get("requires_approval") is True:
return True
# Check risk level
risk_level = ai_use_case_metadata.get("risk_level", "none")
risk_score = RISK_LEVELS.get(risk_level, 0)
# Medium or higher risk always requires review
if risk_score >= RISK_LEVELS["medium"]:
return True
# Check if this specific action is in the reviewed_actions list
reviewed_actions = ai_use_case_metadata.get("reviewed_actions", [])
if action in reviewed_actions:
return True
# Check if action is inherently high-risk
if action in HIGH_RISK_ACTIONS and risk_score >= RISK_LEVELS["low"]:
return True
return False
async def check_decision_guard(
db: AsyncSession,
tenant_id: uuid.UUID,
instance_id: uuid.UUID,
step_config: dict[str, Any],
action: str,
ai_use_case_metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Check the automated-decision guard for a workflow step.
Returns a dict with:
- ``allowed``: bool — whether the action can proceed automatically
- ``requires_approval``: bool — whether human approval is needed
- ``reason``: str — explanation if blocked
- ``approval_action``: str — action to create approval for
If the guard blocks, the workflow engine should pause and create
an ApprovalRequest instead of executing the action.
"""
# Merge step-level and agent-level metadata
step_metadata = step_config.get("ai_use_case_metadata")
metadata = ai_use_case_metadata or step_metadata
if not requires_human_review(metadata, action):
return {
"allowed": True,
"requires_approval": False,
"reason": None,
}
# Human review required
risk_level = (metadata or {}).get("risk_level", "medium")
return {
"allowed": False,
"requires_approval": True,
"reason": (
f"Action '{action}' requires human review "
f"(risk level: {risk_level})"
),
"approval_action": action,
"risk_level": risk_level,
}
__all__ = [
"requires_human_review",
"check_decision_guard",
"RISK_LEVELS",
"HIGH_RISK_ACTIONS",
]