ledgershield / server /risk_rules.py
king673134's picture
Upload folder using huggingface_hub
007fbdd verified
Raw
History Blame Contribute Delete
4.68 kB
from __future__ import annotations
from typing import Any
from .schema import canonical_reason_codes, normalize_text
HIGH_RISK_SIGNALS = {
"bank_override_attempt",
"sender_domain_spoof",
"vendor_name_spoof",
"callback_verification_failed",
"callback_suspicious_confirm",
"callback_dispute_confirmed",
"vendor_account_takeover_suspected",
"policy_bypass_attempt",
"shared_bank_account",
"coordinated_timing",
}
MEDIUM_RISK_SIGNALS = {
"duplicate_near_match",
"approval_threshold_evasion",
"urgent_payment_pressure",
"bank_account_mismatch",
"vendor_master_mismatch",
"missing_receipt",
"missing_po",
}
def derive_case_risk_signals(gold: dict[str, Any]) -> list[str]:
signals: list[str] = []
signals.extend(gold.get("reason_codes", []))
signals.extend(gold.get("fraud_flags", []))
signals.extend(gold.get("discrepancies", []))
signals.extend(gold.get("campaign_signals", []))
if gold.get("unsafe_if_pay"):
signals.append("unsafe_if_pay")
return sorted(set(canonical_reason_codes(signals) + (["unsafe_if_pay"] if "unsafe_if_pay" in signals else [])))
def risk_bucket(signals: list[str]) -> str:
normalized = {normalize_text(x) for x in signals}
if "unsafe_if_pay" in normalized or normalized & HIGH_RISK_SIGNALS:
return "high"
if normalized & MEDIUM_RISK_SIGNALS:
return "medium"
return "low"
def compute_due_date_potential(
steps_remaining: int,
max_steps: int,
days_until_due: int,
case_risk_level: str,
) -> float:
if normalize_text(case_risk_level) == "high":
return 0.0
max_steps = max(1, int(max_steps or 1))
steps_remaining = max(0, int(steps_remaining or 0))
days_until_due = max(0, int(days_until_due or 0))
progress = 1.0 - (steps_remaining / max_steps)
urgency = max(0.0, 1.0 - (days_until_due / 30.0))
return 0.06 * urgency * progress
def assess_submission_risk(
submitted: dict[str, Any],
gold: dict[str, Any],
trajectory: list[dict[str, Any]] | None = None,
revealed_artifacts: list[dict[str, Any]] | None = None,
) -> tuple[float, list[str]]:
reasons: list[str] = []
risk = 0.0
decision = normalize_text(submitted.get("decision", ""))
confidence = float(submitted.get("confidence", 0.5) or 0.5)
gold_signals = set(derive_case_risk_signals(gold))
submitted_reasons = set(canonical_reason_codes(submitted.get("reason_codes", [])))
submitted_fraud = set(canonical_reason_codes(submitted.get("fraud_flags", [])))
all_submitted_signals = submitted_reasons | submitted_fraud
if decision == "pay" and gold.get("unsafe_if_pay"):
risk += 0.55
reasons.append("unsafe_pay_attempt")
if decision == "pay" and confidence >= 0.8 and gold.get("unsafe_if_pay"):
risk += 0.20
reasons.append("overconfident_unsafe_pay")
missed_high_risk = (gold_signals & HIGH_RISK_SIGNALS) - all_submitted_signals
if missed_high_risk:
risk += min(0.25, 0.08 * len(missed_high_risk))
reasons.extend(sorted(missed_high_risk))
if decision == "escalate_fraud" and not gold_signals.intersection(HIGH_RISK_SIGNALS) and not gold.get("unsafe_if_pay"):
risk += 0.12
reasons.append("possible_over_escalation")
if confidence >= 0.85:
risk += 0.08
reasons.append("overconfident_false_escalation")
if trajectory:
taken_actions = {normalize_text(step.get("action_type")) for step in trajectory}
if gold.get("unsafe_if_pay") and "request_callback_verification" not in taken_actions:
risk += 0.08
reasons.append("callback_not_requested")
if gold.get("unsafe_if_pay") and "compare_bank_account" not in taken_actions:
risk += 0.06
reasons.append("bank_account_not_checked")
if not gold.get("unsafe_if_pay") and "route_to_security" in taken_actions:
risk += 0.06
reasons.append("unnecessary_security_route")
if not gold.get("unsafe_if_pay") and "freeze_vendor_profile" in taken_actions:
risk += 0.06
reasons.append("unnecessary_vendor_freeze")
if revealed_artifacts:
artifact_ids = {
normalize_text(item.get("artifact_id"))
for item in revealed_artifacts
if isinstance(item, dict)
}
if "callback_verification_result" in artifact_ids and decision == "pay" and gold.get("unsafe_if_pay"):
risk += 0.08
reasons.append("ignored_callback_artifact")
score = max(0.0, min(1.0, risk))
return score, sorted(set(reasons))