Skip to content

Commit d03a5f5

Browse files
committed
feat(pipeline): Universal Pipeline Dashboard with Bulk Ingestion and Review Queue
1 parent 6a96965 commit d03a5f5

1 file changed

Lines changed: 238 additions & 0 deletions

File tree

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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

Comments
 (0)