Spaces:
Sleeping
Sleeping
| 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)) | |