|
import gradio as gr |
|
import os |
|
from typing import List, Dict, Tuple |
|
import json |
|
import datetime |
|
|
|
|
|
def validate_documents(files: List[str]) -> Tuple[str, str]: |
|
"""Simulate Intake Agent: Check if all required docs are present.""" |
|
if not files: |
|
return "β No documents uploaded", "error" |
|
|
|
required = ["ID", "Pay Stubs", "W-2", "Tax Returns", "Bank Statements", "Offer", "Insurance", "Title"] |
|
uploaded = [os.path.basename(f).split(".")[0] for f in files] |
|
missing = [doc for doc in required if doc not in uploaded] |
|
|
|
if missing: |
|
return f"β οΈ Missing documents: {', '.join(missing)}", "warning" |
|
return "β
All documents validated successfully", "success" |
|
|
|
def extract_data(files: List[str], ssn: str) -> Dict: |
|
"""Simulate Extraction Agent: Extract data from documents.""" |
|
if not files or not ssn: |
|
return {} |
|
|
|
return { |
|
"application_id": "APP-2024-001234", |
|
"ssn": ssn, |
|
"applicant_name": "John Doe", |
|
"monthly_income": 8500, |
|
"monthly_debts": 2100, |
|
"credit_score": 720, |
|
"property_value": 450000, |
|
"loan_amount": 360000, |
|
"insurance_coverage": "12_months", |
|
"title_status": "Clear", |
|
"employment_status": "Full-time", |
|
"years_employed": 3.5 |
|
} |
|
|
|
def analyze_data(data: Dict) -> Dict: |
|
"""Simulate Credit/Capacity/Collateral/Compliance Agents: Analyze data.""" |
|
if not data: |
|
return {} |
|
|
|
dti = (data["monthly_debts"] / data["monthly_income"]) * 100 |
|
ltv = (data["loan_amount"] / data["property_value"]) * 100 |
|
flags = [] |
|
risk_level = "Low" |
|
|
|
if dti > 45: |
|
flags.append("High DTI Ratio") |
|
risk_level = "High" |
|
elif dti > 36: |
|
flags.append("Elevated DTI Ratio") |
|
risk_level = "Medium" |
|
|
|
if data["credit_score"] < 620: |
|
flags.append("Low Credit Score") |
|
risk_level = "High" |
|
elif data["credit_score"] < 680: |
|
flags.append("Fair Credit Score") |
|
if risk_level != "High": |
|
risk_level = "Medium" |
|
|
|
if ltv > 95: |
|
flags.append("High LTV Ratio") |
|
risk_level = "High" |
|
elif ltv > 80: |
|
flags.append("Elevated LTV Ratio") |
|
if risk_level == "Low": |
|
risk_level = "Medium" |
|
|
|
if "Clear" not in data["title_status"]: |
|
flags.append("Title Issues") |
|
risk_level = "High" |
|
|
|
status = "Ready for Approval" if not flags else "Requires Review" |
|
if risk_level == "High": |
|
status = "High Risk - Manual Review Required" |
|
|
|
return { |
|
"application_id": data["application_id"], |
|
"applicant_name": data["applicant_name"], |
|
"dti_ratio": round(dti, 2), |
|
"ltv_ratio": round(ltv, 2), |
|
"credit_score": data["credit_score"], |
|
"monthly_income": f"${data['monthly_income']:,}", |
|
"monthly_debts": f"${data['monthly_debts']:,}", |
|
"loan_amount": f"${data['loan_amount']:,}", |
|
"property_value": f"${data['property_value']:,}", |
|
"risk_level": risk_level, |
|
"flags": flags, |
|
"status": status, |
|
"processed_date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
|
} |
|
|
|
def handle_conditions(additional_files: List[str], comments: str) -> str: |
|
"""Simulate Conditions Agent: Process additional uploads.""" |
|
if additional_files and comments.strip(): |
|
file_names = [os.path.basename(f) for f in additional_files] |
|
return f"β
Additional documentation received: {', '.join(file_names)}\n\nπ Comments: {comments}\n\nβ³ Status: Under Review" |
|
elif additional_files: |
|
file_names = [os.path.basename(f) for f in additional_files] |
|
return f"β
Additional files uploaded: {', '.join(file_names)}\n\nβ οΈ Please provide comments for review" |
|
elif comments.strip(): |
|
return f"π Comments noted: {comments}\n\nβ οΈ Please upload supporting documents" |
|
return "β No additional files or comments provided" |
|
|
|
def finalize_approval(decision: str, comments: str, analysis: Dict) -> str: |
|
"""Simulate Final Approval Agent: Finalize decision.""" |
|
if not analysis: |
|
return "β Error: No analysis data available. Please complete analysis first." |
|
|
|
app_id = analysis.get("application_id", "N/A") |
|
applicant = analysis.get("applicant_name", "N/A") |
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
|
|
|
if decision == "Approve": |
|
if not analysis.get("flags", []): |
|
return f""" |
|
π **APPLICATION APPROVED** |
|
|
|
π **Application ID:** {app_id} |
|
π€ **Applicant:** {applicant} |
|
π
**Decision Date:** {timestamp} |
|
β
**Status:** Clear to Close |
|
|
|
π¦ **Next Steps:** |
|
- Closing package will be generated automatically |
|
- Borrower will be contacted within 24 hours |
|
- Expected closing date: {(datetime.datetime.now() + datetime.timedelta(days=14)).strftime("%Y-%m-%d")} |
|
|
|
π¬ **Comments:** {comments if comments.strip() else 'Standard approval - all criteria met'} |
|
""" |
|
else: |
|
return f""" |
|
β
**APPLICATION APPROVED WITH CONDITIONS** |
|
|
|
π **Application ID:** {app_id} |
|
π€ **Applicant:** {applicant} |
|
π
**Decision Date:** {timestamp} |
|
β οΈ **Conditions:** {', '.join(analysis['flags'])} |
|
|
|
π **Required Actions:** |
|
- Address flagged conditions before closing |
|
- Submit additional documentation if required |
|
- Schedule manual review if needed |
|
|
|
π¬ **Comments:** {comments} |
|
""" |
|
else: |
|
return f""" |
|
β **APPLICATION REJECTED** |
|
|
|
π **Application ID:** {app_id} |
|
π€ **Applicant:** {applicant} |
|
π
**Decision Date:** {timestamp} |
|
|
|
π« **Rejection Reasons:** |
|
{chr(10).join([f"β’ {flag}" for flag in analysis.get('flags', ['Manual rejection'])])} |
|
|
|
π **Next Steps:** |
|
- Applicant will be contacted within 48 hours |
|
- Adverse action notice will be sent |
|
- Reapplication possible after addressing issues |
|
|
|
π¬ **Comments:** {comments} |
|
""" |
|
|
|
|
|
custom_css = """ |
|
/* Global Styles */ |
|
.gradio-container { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
|
min-height: 100vh; |
|
} |
|
|
|
.main { |
|
background: rgba(255, 255, 255, 0.95) !important; |
|
border-radius: 20px !important; |
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1) !important; |
|
margin: 20px !important; |
|
padding: 0 !important; |
|
} |
|
|
|
/* Header Styling */ |
|
.header-section { |
|
background: linear-gradient(90deg, #1e3c72 0%, #2a5298 100%) !important; |
|
color: white !important; |
|
padding: 30px !important; |
|
border-radius: 20px 20px 0 0 !important; |
|
margin-bottom: 0 !important; |
|
text-align: center !important; |
|
} |
|
|
|
.header-section h1 { |
|
margin: 0 !important; |
|
font-size: 2.5rem !important; |
|
font-weight: 700 !important; |
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3) !important; |
|
} |
|
|
|
.header-section p { |
|
margin: 10px 0 0 0 !important; |
|
font-size: 1.1rem !important; |
|
opacity: 0.9 !important; |
|
} |
|
|
|
/* Section Styling */ |
|
.section-card { |
|
background: white !important; |
|
border-radius: 15px !important; |
|
padding: 25px !important; |
|
margin: 20px !important; |
|
box-shadow: 0 8px 25px rgba(0,0,0,0.1) !important; |
|
border: 1px solid rgba(0,0,0,0.05) !important; |
|
transition: all 0.3s ease !important; |
|
} |
|
|
|
.section-card:hover { |
|
transform: translateY(-2px) !important; |
|
box-shadow: 0 12px 35px rgba(0,0,0,0.15) !important; |
|
} |
|
|
|
.section-title { |
|
color: #2c3e50 !important; |
|
font-size: 1.5rem !important; |
|
font-weight: 600 !important; |
|
margin-bottom: 20px !important; |
|
padding-bottom: 10px !important; |
|
border-bottom: 3px solid #3498db !important; |
|
display: flex !important; |
|
align-items: center !important; |
|
gap: 10px !important; |
|
} |
|
|
|
/* Button Styling */ |
|
.btn-primary { |
|
background: linear-gradient(45deg, #667eea, #764ba2) !important; |
|
border: none !important; |
|
border-radius: 10px !important; |
|
padding: 12px 30px !important; |
|
font-weight: 600 !important; |
|
font-size: 1rem !important; |
|
transition: all 0.3s ease !important; |
|
text-transform: uppercase !important; |
|
letter-spacing: 0.5px !important; |
|
} |
|
|
|
.btn-primary:hover { |
|
transform: translateY(-2px) !important; |
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4) !important; |
|
} |
|
|
|
.btn-success { |
|
background: linear-gradient(45deg, #56ab2f, #a8e6cf) !important; |
|
border: none !important; |
|
border-radius: 10px !important; |
|
padding: 12px 30px !important; |
|
font-weight: 600 !important; |
|
} |
|
|
|
.btn-danger { |
|
background: linear-gradient(45deg, #ff416c, #ff4b2b) !important; |
|
border: none !important; |
|
border-radius: 10px !important; |
|
padding: 12px 30px !important; |
|
font-weight: 600 !important; |
|
} |
|
|
|
/* Input Styling */ |
|
input, textarea, select { |
|
border-radius: 8px !important; |
|
border: 2px solid #e1e8ed !important; |
|
padding: 12px !important; |
|
font-size: 1rem !important; |
|
transition: all 0.3s ease !important; |
|
} |
|
|
|
input:focus, textarea:focus, select:focus { |
|
border-color: #667eea !important; |
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; |
|
outline: none !important; |
|
} |
|
|
|
/* Status Messages */ |
|
.status-success { |
|
background: linear-gradient(45deg, #56ab2f, #a8e6cf) !important; |
|
color: white !important; |
|
padding: 15px !important; |
|
border-radius: 10px !important; |
|
font-weight: 600 !important; |
|
} |
|
|
|
.status-warning { |
|
background: linear-gradient(45deg, #f7971e, #ffd200) !important; |
|
color: #333 !important; |
|
padding: 15px !important; |
|
border-radius: 10px !important; |
|
font-weight: 600 !important; |
|
} |
|
|
|
.status-error { |
|
background: linear-gradient(45deg, #ff416c, #ff4b2b) !important; |
|
color: white !important; |
|
padding: 15px !important; |
|
border-radius: 10px !important; |
|
font-weight: 600 !important; |
|
} |
|
|
|
/* Progress Indicators */ |
|
.step-indicator { |
|
display: flex !important; |
|
justify-content: space-between !important; |
|
margin: 20px 0 !important; |
|
position: relative !important; |
|
} |
|
|
|
.step { |
|
background: #e1e8ed !important; |
|
border-radius: 50% !important; |
|
width: 40px !important; |
|
height: 40px !important; |
|
display: flex !important; |
|
align-items: center !important; |
|
justify-content: center !important; |
|
font-weight: bold !important; |
|
color: #666 !important; |
|
z-index: 2 !important; |
|
} |
|
|
|
.step.active { |
|
background: linear-gradient(45deg, #667eea, #764ba2) !important; |
|
color: white !important; |
|
} |
|
|
|
.step.completed { |
|
background: linear-gradient(45deg, #56ab2f, #a8e6cf) !important; |
|
color: white !important; |
|
} |
|
|
|
/* JSON Display Enhancement */ |
|
.json-display { |
|
background: #f8f9fa !important; |
|
border: 1px solid #e9ecef !important; |
|
border-radius: 10px !important; |
|
padding: 20px !important; |
|
font-family: 'Monaco', 'Consolas', monospace !important; |
|
max-height: 400px !important; |
|
overflow-y: auto !important; |
|
} |
|
|
|
/* File Upload Styling */ |
|
.file-upload { |
|
border: 2px dashed #667eea !important; |
|
border-radius: 10px !important; |
|
padding: 30px !important; |
|
text-align: center !important; |
|
background: rgba(102, 126, 234, 0.05) !important; |
|
transition: all 0.3s ease !important; |
|
} |
|
|
|
.file-upload:hover { |
|
background: rgba(102, 126, 234, 0.1) !important; |
|
border-color: #5a67d8 !important; |
|
} |
|
|
|
/* Responsive Design */ |
|
@media (max-width: 768px) { |
|
.main { |
|
margin: 10px !important; |
|
} |
|
|
|
.section-card { |
|
margin: 10px !important; |
|
padding: 20px !important; |
|
} |
|
|
|
.header-section h1 { |
|
font-size: 2rem !important; |
|
} |
|
} |
|
""" |
|
|
|
|
|
with gr.Blocks(css=custom_css, title="Mortgage Underwriting System", theme=gr.themes.Soft()) as app: |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.HTML(""" |
|
<div class="header-section"> |
|
<h1>π Mortgage Underwriting Automation</h1> |
|
<p>Intelligent Document Processing & Risk Assessment Platform</p> |
|
</div> |
|
""") |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.HTML(""" |
|
<div class="section-card"> |
|
<div class="step-indicator"> |
|
<div class="step active">1</div> |
|
<div class="step">2</div> |
|
<div class="step">3</div> |
|
<div class="step">4</div> |
|
</div> |
|
<div style="text-align: center; margin-top: 10px; color: #666;"> |
|
<strong>Document Upload</strong> β Data Analysis β Conditions β Final Approval |
|
</div> |
|
</div> |
|
""") |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.HTML('<div class="section-card">') |
|
gr.HTML('<div class="section-title">π Document Upload & Validation</div>') |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=2): |
|
files_input = gr.File( |
|
label="Required Documents", |
|
file_count="multiple", |
|
file_types=[".pdf", ".png", ".jpg", ".jpeg"], |
|
elem_classes=["file-upload"] |
|
) |
|
gr.HTML(""" |
|
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 10px 0;"> |
|
<strong>π Required Documents Checklist:</strong><br> |
|
β’ Government-issued ID<br> |
|
β’ Recent Pay Stubs (2-3 months)<br> |
|
β’ W-2 Forms (2 years)<br> |
|
β’ Tax Returns (2 years)<br> |
|
β’ Bank Statements (3 months)<br> |
|
β’ Purchase Offer/Contract<br> |
|
β’ Insurance Documentation<br> |
|
β’ Property Title Information |
|
</div> |
|
""") |
|
|
|
with gr.Column(scale=1): |
|
ssn_input = gr.Textbox( |
|
label="Social Security Number", |
|
placeholder="XXX-XX-XXXX", |
|
type="password" |
|
) |
|
validate_btn = gr.Button("π Validate Documents", variant="primary", size="lg") |
|
validate_output = gr.Textbox( |
|
label="Validation Status", |
|
interactive=False, |
|
lines=2 |
|
) |
|
|
|
gr.HTML('</div>') |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.HTML('<div class="section-card">') |
|
gr.HTML('<div class="section-title">π Automated Analysis & Risk Assessment</div>') |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
extract_btn = gr.Button("β‘ Extract Data & Analyze Risk", variant="primary", size="lg") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
analysis_output = gr.JSON( |
|
label="π Comprehensive Analysis Results", |
|
elem_classes=["json-display"] |
|
) |
|
|
|
with gr.Column(): |
|
gr.HTML(""" |
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px;"> |
|
<h3 style="margin: 0 0 15px 0;">π― Risk Assessment Criteria</h3> |
|
<div style="background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px;"> |
|
<strong>π DTI Ratio:</strong> β€ 45% (Optimal: β€ 36%)<br> |
|
<strong>π³ Credit Score:</strong> β₯ 620 (Preferred: β₯ 680)<br> |
|
<strong>ποΈ LTV Ratio:</strong> β€ 95% (Conventional: β€ 80%)<br> |
|
<strong>π Documentation:</strong> Complete & Verified<br> |
|
<strong>π’ Employment:</strong> Stable Income History |
|
</div> |
|
</div> |
|
""") |
|
|
|
gr.HTML('</div>') |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.HTML('<div class="section-card">') |
|
gr.HTML('<div class="section-title">π Additional Documentation & Conditions</div>') |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
additional_files = gr.File( |
|
label="Upload Additional Documents", |
|
file_count="multiple", |
|
file_types=[".pdf", ".png", ".jpg", ".jpeg"], |
|
elem_classes=["file-upload"] |
|
) |
|
condition_comments = gr.Textbox( |
|
label="Underwriter Comments & Instructions", |
|
placeholder="Provide detailed comments about additional requirements...", |
|
lines=4 |
|
) |
|
condition_btn = gr.Button("π€ Submit Additional Documentation", variant="secondary", size="lg") |
|
|
|
with gr.Column(): |
|
condition_output = gr.Textbox( |
|
label="π Conditions Status", |
|
interactive=False, |
|
lines=6 |
|
) |
|
|
|
gr.HTML(""" |
|
<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 8px; color: #856404;"> |
|
<strong>π‘ Common Additional Requirements:</strong><br> |
|
β’ Gift Letter (for down payment assistance)<br> |
|
β’ Employment Verification Letter<br> |
|
β’ Asset Verification Documents<br> |
|
β’ Property Appraisal Report<br> |
|
β’ Explanation Letters (for credit items)<br> |
|
β’ Homeowner's Insurance Proof |
|
</div> |
|
""") |
|
|
|
gr.HTML('</div>') |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.HTML('<div class="section-card">') |
|
gr.HTML('<div class="section-title">βοΈ Final Underwriting Decision</div>') |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
approve_radio = gr.Radio( |
|
choices=["Approve", "Reject"], |
|
label="π Underwriting Decision", |
|
info="Select final decision based on comprehensive analysis" |
|
) |
|
approval_comments = gr.Textbox( |
|
label="π¬ Decision Comments & Rationale", |
|
placeholder="Provide detailed reasoning for the decision...", |
|
lines=4 |
|
) |
|
approve_btn = gr.Button("β
Submit Final Decision", variant="primary", size="lg") |
|
|
|
with gr.Column(): |
|
final_output = gr.Textbox( |
|
label="π Final Approval Status", |
|
interactive=False, |
|
lines=12, |
|
elem_classes=["status-display"] |
|
) |
|
|
|
gr.HTML('</div>') |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.HTML(""" |
|
<div style="text-align: center; padding: 20px; color: #666; background: rgba(0,0,0,0.05); border-radius: 0 0 20px 20px;"> |
|
<p>π Secure β’ π€ AI-Powered β’ β‘ Real-time Processing</p> |
|
<p><small>Β© 2024 Mortgage Underwriting Automation Platform | Version 2.0</small></p> |
|
</div> |
|
""") |
|
|
|
|
|
validate_btn.click( |
|
fn=lambda files: validate_documents(files)[0], |
|
inputs=files_input, |
|
outputs=validate_output |
|
) |
|
|
|
extract_btn.click( |
|
fn=lambda files, ssn: analyze_data(extract_data(files, ssn)), |
|
inputs=[files_input, ssn_input], |
|
outputs=analysis_output |
|
) |
|
|
|
condition_btn.click( |
|
fn=handle_conditions, |
|
inputs=[additional_files, condition_comments], |
|
outputs=condition_output |
|
) |
|
|
|
approve_btn.click( |
|
fn=finalize_approval, |
|
inputs=[approve_radio, approval_comments, analysis_output], |
|
outputs=final_output |
|
) |
|
|
|
if __name__ == "__main__": |
|
app.launch( |
|
server_name="0.0.0.0", |
|
server_port=7860, |
|
share=False, |
|
show_error=True, |
|
favicon_path=None, |
|
ssl_verify=False, |
|
debug=True |
|
) |