import logging import time from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from .base import BaseVendor, DocumentType, VendorResult, VerificationStatus from .registry import registry, VendorRegistry logger = logging.getLogger(__name__) @dataclass class EngineResult: """Consolidated result returned by the VerificationEngine.""" is_success: bool document_type: str status: VerificationStatus successful_vendor: Optional[str] = None data: Dict[str, Any] = field(default_factory=dict) attempts: List[Dict[str, Any]] = field(default_factory=list) error_message: Optional[str] = None total_latency_ms: float = 0.0 def to_dict(self) -> Dict[str, Any]: return { "is_success": self.is_success, "document_type": self.document_type, "status": self.status.value if isinstance(self.status, VerificationStatus) else str(self.status), "successful_vendor": self.successful_vendor, "data": self.data, "attempts_count": len(self.attempts), "attempts": self.attempts, "error_message": self.error_message, "total_latency_ms": round(self.total_latency_ms, 2), } class VerificationEngine: """ Core waterfall orchestrator. Executes multiple vendors in a fallback chain. If vendor A errors or fails, the engine falls back to vendor B, C, etc. """ def __init__(self, vendor_registry: Optional[VendorRegistry] = None): self.registry = vendor_registry or registry def verify( self, document_type: str, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None, preferred_vendor: Optional[str] = None, ) -> EngineResult: """ Executes document verification through the vendor waterfall. """ start_time = time.time() doc_type_clean = document_type.lower() vendors = self.registry.get_vendors_for_document(doc_type_clean, preferred_vendor=preferred_vendor) if not vendors: total_latency = (time.time() - start_time) * 1000 return EngineResult( is_success=False, document_type=doc_type_clean, status=VerificationStatus.ERROR, error_message=f"No active vendors configured for document type '{document_type}'", total_latency_ms=total_latency, ) attempts: List[Dict[str, Any]] = [] logger.info( "Starting verification for '%s'. Candidate vendors in order: %s", doc_type_clean, [v.name for v in vendors], ) for attempt_index, vendor in enumerate(vendors, start=1): logger.info( "Attempt #%d: Dispatching verification to vendor '%s'", attempt_index, vendor.name, ) result: VendorResult = vendor.verify(doc_type_clean, data, files) attempt_record = result.to_dict() attempt_record["attempt_number"] = attempt_index attempts.append(attempt_record) if result.is_success: total_latency = (time.time() - start_time) * 1000 logger.info( "Verification SUCCEEDED via vendor '%s' on attempt #%d (latency=%.2f ms)", vendor.name, attempt_index, result.latency_ms, ) return EngineResult( is_success=True, document_type=doc_type_clean, status=VerificationStatus.SUCCESS, successful_vendor=vendor.name, data=result.data, attempts=attempts, total_latency_ms=total_latency, ) else: logger.warning( "Vendor '%s' failed on attempt #%d: status=%s, code=%s, error=%s. Triggering fallback to next vendor.", vendor.name, attempt_index, result.status, result.error_code, result.error_message, ) # If all vendors failed total_latency = (time.time() - start_time) * 1000 last_error = attempts[-1].get("error_message") if attempts else "All vendors failed" logger.error( "All %d vendors failed verification for '%s'. Total time=%.2f ms", len(vendors), doc_type_clean, total_latency, ) last_data = attempts[-1].get("data") if attempts and isinstance(attempts[-1].get("data"), dict) else {} return EngineResult( is_success=False, document_type=doc_type_clean, status=VerificationStatus.FAILED, successful_vendor=None, data=last_data, attempts=attempts, error_message=last_error, total_latency_ms=total_latency, ) # Global default engine instance engine = VerificationEngine()