Dhurgh's picture
Initial commit
410242f
Raw
History Blame Contribute Delete
11 kB
"""
Validator Agent - Validates extracted data and ensures data quality.
Performs consistency checks, format validation, and anomaly detection.
"""
import re
import logging
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
logger = logging.getLogger(__name__)
class ValidationStatus(str, Enum):
"""Validation status"""
VALID = "valid"
WARNING = "warning"
ERROR = "error"
UNKNOWN = "unknown"
@dataclass
class ValidationIssue:
"""A validation issue found"""
field: str
issue_type: str
message: str
severity: ValidationStatus
suggestion: Optional[str] = None
@dataclass
class ValidationResult:
"""Result from data validation"""
status: ValidationStatus
is_valid: bool
issues: List[ValidationIssue]
warnings: List[ValidationIssue]
data_quality_score: float
confidence: float
class DataValidator:
"""Validate extracted data quality"""
# Validation rules for different field types
VALIDATION_RULES = {
'email': {
'pattern': r'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$',
'required': False,
'max_length': 100,
'description': 'Must be valid email format'
},
'phone': {
'pattern': r'^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$',
'required': False,
'description': 'Must be valid phone format'
},
'date': {
'pattern': r'^\d{4}-\d{2}-\d{2}$|^\d{1,2}/\d{1,2}/\d{2,4}$',
'required': False,
'description': 'Must be valid date format'
},
'amount': {
'pattern': r'^\d+(\.\d{2})?$',
'required': False,
'min_value': 0,
'description': 'Must be positive number'
},
'invoice_number': {
'required': True,
'min_length': 1,
'max_length': 50,
'description': 'Invoice number is required'
},
}
def __init__(self, strict_mode: bool = False):
"""Initialize validator"""
self.strict_mode = strict_mode
self.compiled_patterns = {}
self._compile_patterns()
def _compile_patterns(self):
"""Compile regex patterns"""
for field, rules in self.VALIDATION_RULES.items():
if 'pattern' in rules:
try:
self.compiled_patterns[field] = re.compile(rules['pattern'])
except Exception as e:
logger.warning(f"Failed to compile pattern for {field}: {e}")
def validate(
self,
data: Dict[str, Any],
schema: Optional[Dict] = None
) -> ValidationResult:
"""
Validate extracted data against schema.
Args:
data: Data to validate
schema: Validation schema (uses defaults if not provided)
Returns:
ValidationResult with issues and quality score
"""
try:
issues = []
warnings = []
# Use provided schema or default
validation_schema = schema or self._get_default_schema(data)
# Validate each field
for field_name, field_value in data.items():
field_issues = self._validate_field(
field_name,
field_value,
validation_schema.get(field_name, {})
)
if field_issues:
for issue in field_issues:
if issue.severity == ValidationStatus.ERROR:
issues.append(issue)
else:
warnings.append(issue)
# Calculate quality score
quality_score = self._calculate_quality_score(data, issues)
# Determine overall status
if issues:
status = ValidationStatus.ERROR
is_valid = False
elif warnings:
status = ValidationStatus.WARNING
is_valid = True
else:
status = ValidationStatus.VALID
is_valid = True
return ValidationResult(
status=status,
is_valid=is_valid,
issues=issues,
warnings=warnings,
data_quality_score=quality_score,
confidence=1.0 - (len(issues) * 0.1)
)
except Exception as e:
logger.error(f"Validation failed: {str(e)}")
return ValidationResult(
status=ValidationStatus.UNKNOWN,
is_valid=False,
issues=[],
warnings=[],
data_quality_score=0.0,
confidence=0.0
)
def _validate_field(
self,
field_name: str,
field_value: Any,
rules: Dict
) -> List[ValidationIssue]:
"""Validate a single field"""
issues = []
if field_value is None or field_value == '':
if rules.get('required', False):
issues.append(ValidationIssue(
field=field_name,
issue_type='missing_required',
message=f"{field_name} is required",
severity=ValidationStatus.ERROR,
suggestion=f"Provide a value for {field_name}"
))
return issues
value_str = str(field_value)
# Check length
if 'min_length' in rules:
if len(value_str) < rules['min_length']:
issues.append(ValidationIssue(
field=field_name,
issue_type='too_short',
message=f"{field_name} is too short",
severity=ValidationStatus.WARNING
))
if 'max_length' in rules:
if len(value_str) > rules['max_length']:
issues.append(ValidationIssue(
field=field_name,
issue_type='too_long',
message=f"{field_name} exceeds max length",
severity=ValidationStatus.WARNING
))
# Check format/pattern
if 'pattern' in rules:
pattern = self.compiled_patterns.get(field_name)
if pattern and not pattern.match(value_str):
issues.append(ValidationIssue(
field=field_name,
issue_type='invalid_format',
message=f"{field_name} format is invalid",
severity=ValidationStatus.ERROR if self.strict_mode else ValidationStatus.WARNING,
suggestion=rules.get('description')
))
# Check numeric constraints
if 'min_value' in rules:
try:
num_value = float(value_str.replace('$', '').replace(',', ''))
if num_value < rules['min_value']:
issues.append(ValidationIssue(
field=field_name,
issue_type='value_too_low',
message=f"{field_name} is below minimum value",
severity=ValidationStatus.WARNING
))
except ValueError:
pass
if 'max_value' in rules:
try:
num_value = float(value_str.replace('$', '').replace(',', ''))
if num_value > rules['max_value']:
issues.append(ValidationIssue(
field=field_name,
issue_type='value_too_high',
message=f"{field_name} exceeds maximum value",
severity=ValidationStatus.WARNING
))
except ValueError:
pass
return issues
def _get_default_schema(self, data: Dict) -> Dict:
"""Generate default schema from data"""
schema = {}
for field_name in data.keys():
# Match against known rules
if field_name in self.VALIDATION_RULES:
schema[field_name] = self.VALIDATION_RULES[field_name]
else:
# Generic text validation
schema[field_name] = {'required': False}
return schema
def _calculate_quality_score(
self,
data: Dict[str, Any],
issues: List[ValidationIssue]
) -> float:
"""Calculate overall data quality score"""
if not data:
return 0.0
# Base score
score = 1.0
# Deduct for issues
score -= len(issues) * 0.1
# Deduct for missing optional fields
total_fields = len(data)
non_empty_fields = sum(1 for v in data.values() if v is not None and v != '')
if total_fields > 0:
completeness = non_empty_fields / total_fields
score *= completeness
return max(0.0, min(1.0, score))
def validate_consistency(self, records: List[Dict]) -> Dict[str, Any]:
"""Check consistency across multiple records"""
if not records:
return {'valid': True, 'issues': []}
consistency_issues = []
# Check for duplicate data
seen_invoices = set()
for record in records:
invoice_id = record.get('invoice_number', '')
if invoice_id in seen_invoices:
consistency_issues.append({
'type': 'duplicate_invoice',
'value': invoice_id,
'severity': 'warning'
})
seen_invoices.add(invoice_id)
# Check for anomalies
amounts = []
for record in records:
amount = record.get('total_amount')
if amount:
try:
amounts.append(float(str(amount).replace('$', '').replace(',', '')))
except ValueError:
pass
if amounts:
avg_amount = sum(amounts) / len(amounts)
for i, amount in enumerate(amounts):
if amount > avg_amount * 2:
consistency_issues.append({
'type': 'anomaly_detected',
'record_index': i,
'value': amount,
'average': avg_amount,
'severity': 'warning'
})
return {
'valid': len(consistency_issues) == 0,
'issues': consistency_issues
}