Spaces:
Runtime error
Runtime error
| """ | |
| 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" | |
| class ValidationIssue: | |
| """A validation issue found""" | |
| field: str | |
| issue_type: str | |
| message: str | |
| severity: ValidationStatus | |
| suggestion: Optional[str] = None | |
| 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 | |
| } | |