|
| 1 | +""" |
| 2 | +Universal Document Pipeline - Backend Mock Service |
| 3 | +Maps to Challenge 2 Requirements for Document AI |
| 4 | +""" |
| 5 | +from typing import List, Dict, Any, Optional |
| 6 | +from dataclasses import dataclass |
| 7 | +from datetime import datetime |
| 8 | +import random |
| 9 | +import re |
| 10 | + |
| 11 | + |
| 12 | +@dataclass |
| 13 | +class DocumentResult: |
| 14 | + doc_id: str |
| 15 | + filename: str |
| 16 | + doc_type: str |
| 17 | + layouts: List[str] |
| 18 | + validation_status: str |
| 19 | + validation_errors: List[Dict[str, Any]] |
| 20 | + extracted_data: Dict[str, Any] |
| 21 | + confidence: float |
| 22 | + |
| 23 | + |
| 24 | +class DocumentPipeline: |
| 25 | + """ |
| 26 | + Universal Document Pipeline for processing financial documents. |
| 27 | + |
| 28 | + Maps to Challenge Requirements: |
| 29 | + - Auto classify document type |
| 30 | + - Detect layouts (tables, handwritten, stamps) |
| 31 | + - Balance continuity checks |
| 32 | + - Date sequencing validation |
| 33 | + - Cross-document consistency |
| 34 | + - Error clustering |
| 35 | + """ |
| 36 | + |
| 37 | + DOC_TYPES = ["INVOICE", "PAYSLIP", "STATEMENT", "CONTRACT", "RECEIPT", "KYC"] |
| 38 | + LAYOUTS = ["Table", "Handwritten", "Stamp", "Header", "Multi-Table", "Signature"] |
| 39 | + |
| 40 | + def __init__(self): |
| 41 | + self._doc_counter = 1000 |
| 42 | + self._history = {} # For cross-document consistency |
| 43 | + |
| 44 | + def ingest_batch(self, files: List[str]) -> List[DocumentResult]: |
| 45 | + """ |
| 46 | + Simulates parallel batch processing of documents. |
| 47 | + Returns processed results for each file. |
| 48 | + """ |
| 49 | + results = [] |
| 50 | + for filename in files: |
| 51 | + result = self._process_document(filename) |
| 52 | + results.append(result) |
| 53 | + self._doc_counter += 1 |
| 54 | + return results |
| 55 | + |
| 56 | + def _process_document(self, filename: str) -> DocumentResult: |
| 57 | + """Process a single document through the pipeline.""" |
| 58 | + doc_id = f"DOC-{self._doc_counter}" |
| 59 | + |
| 60 | + # Step 1: Auto-classify based on filename patterns |
| 61 | + doc_type = self.classify_document(filename) |
| 62 | + |
| 63 | + # Step 2: Detect layouts (simulated) |
| 64 | + layouts = self._detect_layouts(filename, doc_type) |
| 65 | + |
| 66 | + # Step 3: Extract data (simulated) |
| 67 | + extracted_data = self._extract_data(doc_type) |
| 68 | + |
| 69 | + # Step 4: Run validation checks |
| 70 | + validation_errors = self.validate_logic(extracted_data, doc_type) |
| 71 | + |
| 72 | + # Determine overall status |
| 73 | + if validation_errors: |
| 74 | + status = "FAIL" if any(e["severity"] == "error" for e in validation_errors) else "REVIEW" |
| 75 | + else: |
| 76 | + status = "PASS" |
| 77 | + |
| 78 | + # Calculate confidence |
| 79 | + confidence = self._calculate_confidence(validation_errors) |
| 80 | + |
| 81 | + return DocumentResult( |
| 82 | + doc_id=doc_id, |
| 83 | + filename=filename, |
| 84 | + doc_type=doc_type, |
| 85 | + layouts=layouts, |
| 86 | + validation_status=status, |
| 87 | + validation_errors=validation_errors, |
| 88 | + extracted_data=extracted_data, |
| 89 | + confidence=confidence |
| 90 | + ) |
| 91 | + |
| 92 | + def classify_document(self, filename: str) -> str: |
| 93 | + """ |
| 94 | + Auto-classify document type based on filename patterns. |
| 95 | + Maps to: 'Auto classify document type' |
| 96 | + """ |
| 97 | + filename_lower = filename.lower() |
| 98 | + |
| 99 | + if "invoice" in filename_lower or "inv" in filename_lower: |
| 100 | + return "INVOICE" |
| 101 | + elif "payslip" in filename_lower or "salary" in filename_lower: |
| 102 | + return "PAYSLIP" |
| 103 | + elif "statement" in filename_lower or "bank" in filename_lower: |
| 104 | + return "STATEMENT" |
| 105 | + elif "contract" in filename_lower or "agreement" in filename_lower: |
| 106 | + return "CONTRACT" |
| 107 | + elif "receipt" in filename_lower: |
| 108 | + return "RECEIPT" |
| 109 | + elif "kyc" in filename_lower or "id" in filename_lower: |
| 110 | + return "KYC" |
| 111 | + else: |
| 112 | + return random.choice(self.DOC_TYPES) |
| 113 | + |
| 114 | + def _detect_layouts(self, filename: str, doc_type: str) -> List[str]: |
| 115 | + """ |
| 116 | + Detect document layouts. |
| 117 | + Maps to: 'Detect layouts', 'Table structure recognition' |
| 118 | + """ |
| 119 | + layouts = [] |
| 120 | + |
| 121 | + # Type-based layout detection |
| 122 | + if doc_type == "INVOICE": |
| 123 | + layouts = ["Header", "Table"] |
| 124 | + elif doc_type == "STATEMENT": |
| 125 | + layouts = ["Multi-Table", "Header"] |
| 126 | + elif doc_type == "CONTRACT": |
| 127 | + layouts = ["Header", "Signature"] |
| 128 | + |
| 129 | + # Filename hints |
| 130 | + if "handwritten" in filename.lower(): |
| 131 | + layouts.append("Handwritten") |
| 132 | + if "stamp" in filename.lower(): |
| 133 | + layouts.append("Stamp") |
| 134 | + |
| 135 | + return layouts if layouts else ["Header"] |
| 136 | + |
| 137 | + def _extract_data(self, doc_type: str) -> Dict[str, Any]: |
| 138 | + """Simulate data extraction based on document type.""" |
| 139 | + base_data = { |
| 140 | + "extraction_timestamp": datetime.now().isoformat(), |
| 141 | + "ocr_confidence": round(random.uniform(0.75, 0.98), 2) |
| 142 | + } |
| 143 | + |
| 144 | + if doc_type == "INVOICE": |
| 145 | + base_data.update({ |
| 146 | + "invoice_number": f"INV-{random.randint(1000, 9999)}", |
| 147 | + "invoice_date": "2024-10-01", |
| 148 | + "due_date": "2024-10-30", |
| 149 | + "vendor": {"name": "ACME Corp", "gstin": "27AAACA1234A1ZV"}, |
| 150 | + "subtotal": 45000, |
| 151 | + "tax": 8100, |
| 152 | + "total": 53100, # Intentional mismatch for demo |
| 153 | + }) |
| 154 | + elif doc_type == "STATEMENT": |
| 155 | + base_data.update({ |
| 156 | + "account_number": f"XXXX{random.randint(1000, 9999)}", |
| 157 | + "opening_balance": 100000, |
| 158 | + "closing_balance": 125000, |
| 159 | + "transactions": [ |
| 160 | + {"date": "2024-10-05", "desc": "Credit", "amount": 50000}, |
| 161 | + {"date": "2024-10-15", "desc": "Debit", "amount": -25000} |
| 162 | + ] |
| 163 | + }) |
| 164 | + |
| 165 | + return base_data |
| 166 | + |
| 167 | + def validate_logic(self, data: Dict[str, Any], doc_type: str) -> List[Dict[str, Any]]: |
| 168 | + """ |
| 169 | + Run validation checks on extracted data. |
| 170 | + Maps to: 'Balance continuity checks', 'Date sequencing', 'Cross-document consistency' |
| 171 | + """ |
| 172 | + errors = [] |
| 173 | + |
| 174 | + if doc_type == "INVOICE": |
| 175 | + # Balance Check |
| 176 | + subtotal = data.get("subtotal", 0) |
| 177 | + tax = data.get("tax", 0) |
| 178 | + total = data.get("total", 0) |
| 179 | + if subtotal + tax != total: |
| 180 | + errors.append({ |
| 181 | + "check": "Balance Continuity", |
| 182 | + "message": f"Subtotal ({subtotal}) + Tax ({tax}) != Total ({total})", |
| 183 | + "severity": "error", |
| 184 | + "expected": subtotal + tax, |
| 185 | + "actual": total |
| 186 | + }) |
| 187 | + |
| 188 | + # Date Sequencing |
| 189 | + inv_date = data.get("invoice_date", "") |
| 190 | + due_date = data.get("due_date", "") |
| 191 | + if inv_date and due_date and inv_date > due_date: |
| 192 | + errors.append({ |
| 193 | + "check": "Date Sequencing", |
| 194 | + "message": f"Invoice Date ({inv_date}) > Due Date ({due_date})", |
| 195 | + "severity": "error" |
| 196 | + }) |
| 197 | + |
| 198 | + elif doc_type == "STATEMENT": |
| 199 | + # Balance Continuity for Statements |
| 200 | + opening = data.get("opening_balance", 0) |
| 201 | + closing = data.get("closing_balance", 0) |
| 202 | + transactions = data.get("transactions", []) |
| 203 | + calculated_closing = opening + sum(t.get("amount", 0) for t in transactions) |
| 204 | + |
| 205 | + if calculated_closing != closing: |
| 206 | + errors.append({ |
| 207 | + "check": "Statement Reconciliation", |
| 208 | + "message": f"Calculated closing ({calculated_closing}) != Reported ({closing})", |
| 209 | + "severity": "warning" |
| 210 | + }) |
| 211 | + |
| 212 | + return errors |
| 213 | + |
| 214 | + def _calculate_confidence(self, errors: List[Dict]) -> float: |
| 215 | + """Calculate overall confidence score based on validation results.""" |
| 216 | + base = 0.95 |
| 217 | + for error in errors: |
| 218 | + if error.get("severity") == "error": |
| 219 | + base -= 0.15 |
| 220 | + else: |
| 221 | + base -= 0.05 |
| 222 | + return max(0.4, round(base, 2)) |
| 223 | + |
| 224 | + def get_error_clusters(self, results: List[DocumentResult]) -> Dict[str, int]: |
| 225 | + """ |
| 226 | + Cluster errors for dashboard display. |
| 227 | + Maps to: 'Error clustering' |
| 228 | + """ |
| 229 | + clusters = {} |
| 230 | + for result in results: |
| 231 | + for error in result.validation_errors: |
| 232 | + check = error.get("check", "Unknown") |
| 233 | + clusters[check] = clusters.get(check, 0) + 1 |
| 234 | + return clusters |
| 235 | + |
| 236 | + |
| 237 | +# Singleton instance for API usage |
| 238 | +pipeline = DocumentPipeline() |
0 commit comments