razorshield-api / src /explanation /validator.py
Vedant Sanjay Jadhav
feat: complete RazorShield AI risk platform
10ec54c
Raw
History Blame Contribute Delete
7.31 kB
"""
validator.py
------------
Deterministic grounding and consistency validator for SLM generated explanations.
Checks JSON schema, decision consistency, severity consistency, numeric grounding,
campaign consistency, unsupported claims / hallucinations, and word count.
"""
from __future__ import annotations
import json
import re
from typing import Any
from src.explanation.schemas import ExplanationInput, ExplanationOutput, GoldExpectation
class GroundingValidator:
"""Deterministic grounding and consistency validator."""
UNSUPPORTED_PATTERNS = [
r"\$\d+(?:,\d+)*(?:\.\d+)?", # Monetary amounts like $50,000 not in evidence
r"\b(?:IP|geolocation|GPS|location|device_fingerprint)\b", # Invented technical metadata
r"\b(?:phishing|skimming|credential_stuffing|bin_attack)\b", # Invented attack techniques
r"\b(?:confirmed_fraud|guaranteed_fraud|100%_fraud)\b", # Claiming certainty not in evidence
]
def parse_and_validate_json(self, raw_text: str) -> tuple[ExplanationOutput | None, list[str]]:
"""Parses raw text into JSON and validates against ExplanationOutput Pydantic schema."""
errors = []
# Extract json chunk if wrapped in markdown code fence
json_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw_text, re.DOTALL)
if json_match:
text_to_parse = json_match.group(1)
else:
json_match_raw = re.search(r"(\{.*?\})", raw_text, re.DOTALL)
text_to_parse = json_match_raw.group(1) if json_match_raw else raw_text
try:
data = json.loads(text_to_parse)
except Exception as e:
errors.append(f"JSON parsing error: {e}")
return None, errors
try:
output = ExplanationOutput(**data)
return output, errors
except Exception as e:
errors.append(f"Pydantic schema validation error: {e}")
return None, errors
def validate_grounding(
self,
input_data: ExplanationInput,
output: ExplanationOutput,
expectation: GoldExpectation | None = None,
) -> dict[str, Any]:
"""
Executes strict deterministic grounding checks.
Returns a detailed evaluation dictionary.
"""
full_text = f"{output.title} {output.summary} {' '.join(output.key_signals)} {output.campaign_context} {output.recommended_action} {output.confidence_note}"
full_text_lower = full_text.lower()
words = full_text.split()
word_count = len(words)
# 1. Decision Consistency
decision_consistent = True
dec_errors = []
if input_data.incident_state == "ALERT":
if "normal activity" in full_text_lower or "no risk" in full_text_lower or "normal situation" in full_text_lower:
decision_consistent = False
dec_errors.append("ALERT state described as normal")
elif input_data.incident_state == "INVESTIGATE":
if "confirmed fraud" in full_text_lower or "normal activity" in full_text_lower:
decision_consistent = False
dec_errors.append("INVESTIGATE state described as confirmed fraud or normal")
elif input_data.incident_state == "NORMAL":
if "high risk incident" in full_text_lower or "severe attack" in full_text_lower:
decision_consistent = False
dec_errors.append("NORMAL state described as severe attack")
# 2. Severity Consistency
severity_consistent = True
sev_errors = []
if input_data.severity == "HIGH":
if "low risk" in full_text_lower or "low severity" in full_text_lower or "minimal concern" in full_text_lower:
severity_consistent = False
sev_errors.append("HIGH severity described as low risk")
elif input_data.severity == "LOW":
if "high severity" in full_text_lower or "critical threat" in full_text_lower:
severity_consistent = False
sev_errors.append("LOW severity described as high severity")
# 3. Campaign Consistency
campaign_consistent = True
camp_errors = []
if input_data.campaign_active:
if "no campaign" in full_text_lower or "inactive campaign" in full_text_lower or "no promo" in full_text_lower:
campaign_consistent = False
camp_errors.append("Active campaign claimed as inactive")
else:
if ("campaign is active" in full_text_lower and "no promotional campaign is active" not in full_text_lower and "no campaign is active" not in full_text_lower) or "promotional sale active" in full_text_lower:
campaign_consistent = False
camp_errors.append("Inactive campaign claimed as active")
# 4. Numeric Grounding Check
numeric_grounded = True
num_errors = []
# Verify fraud_excess_ratio preservation
fe_val = input_data.fraud_excess_ratio
# Match digits around decimal
fe_matches = re.findall(rf"\b{fe_val:.1f}(?:x|0)?\b", full_text, re.IGNORECASE)
# Check for contradictory numbers (e.g. claiming 3.2 when evidence says 8.2)
fe_contradictions = re.findall(r"fraud excess(?: ratio)? (?:is|of) (\d+\.\d+)", full_text, re.IGNORECASE)
for c_val in fe_contradictions:
if abs(float(c_val) - fe_val) > 0.1:
numeric_grounded = False
num_errors.append(f"Contradictory fraud_excess_ratio {c_val} vs evidence {fe_val}")
# 5. Unsupported Claims / Hallucination Detection
hallucination_detected = False
hallucination_errors = []
for pattern in self.UNSUPPORTED_PATTERNS:
match = re.search(pattern, full_text, re.IGNORECASE)
if match:
hallucination_detected = True
hallucination_errors.append(f"Unsupported claim detected matching pattern '{pattern}': '{match.group(0)}'")
if expectation:
for forbidden in expectation.forbidden_claims:
if forbidden.lower() in full_text_lower:
hallucination_detected = True
hallucination_errors.append(f"Forbidden claim present: '{forbidden}'")
# 6. Word Count Check
length_valid = word_count <= 150
is_passed = (
decision_consistent
and severity_consistent
and campaign_consistent
and numeric_grounded
and (not hallucination_detected)
and length_valid
)
all_errors = dec_errors + sev_errors + camp_errors + num_errors + hallucination_errors
if not length_valid:
all_errors.append(f"Word count {word_count} exceeds maximum 150 words")
return {
"passed": is_passed,
"word_count": word_count,
"decision_consistent": decision_consistent,
"severity_consistent": severity_consistent,
"campaign_consistent": campaign_consistent,
"numeric_grounded": numeric_grounded,
"hallucination_detected": hallucination_detected,
"length_valid": length_valid,
"errors": all_errors,
}