CoolFace
Apppublic

bhattyuvraj22/loan-underwriting-env

sourceHugging Faceupdated 6mo agoView on Hugging Face
1likes
App README
πŸ€— Live on Hugging Face: https://huggingface.co/spaces/bhattyuvraj22/loan-underwriting-env

πŸ“Œ Overview

Mortgage underwriting is a high-stakes, rule-governed decision process performed daily by human officers at banks and lenders. This environment simulates that exact workflow, giving AI agents the same data a real underwriter sees and scoring them on the same criteria a real lender would use.

An agent must:

  • β€”Compute DTI (Debt-to-Income) and LTV (Loan-to-Value) ratios
  • β€”Apply policy rules to approve, reject, or escalate each applicant
  • β€”Assign accurate interest rates for approved applicants
  • β€”Identify all applicable risk flags
  • β€”Manage portfolio constraints (capital budgets, risk concentration caps)

This makes it an ideal benchmark for structured financial reasoning β€” rules are explicit and deterministic, but edge cases (borderline DTI, fraud detection, thin files) challenge even frontier models.


🎯 Tasks

#NameDifficultyApplicantsKey Challenge
task_1_easySingle Applicant Underwriting🟒 Easy1DTI/LTV math + correct decision
task_2_mediumBatch with Capital Constraints🟑 Medium6Portfolio budget + risk cap
task_3_hardEdge Case PortfolioπŸ”΄ Hard8Fraud, thin files, borderline DTI

Scoring Breakdown

<details> <summary><b>Task 1 β€” Easy (click to expand)</b></summary>

ComponentWeightCriteria
Decision correctness0.50Exact match: approve / reject / escalate
Interest rate accuracy0.25Within Β±0.5% of ground truth (approve only)
Risk flag recall0.20Fraction of ground-truth flags identified
Risk flag precision0.05Penalty for false-positive flags

</details>

<details> <summary><b>Task 2 β€” Medium (click to expand)</b></summary>

ComponentWeightCriteria
Decision accuracy0.40Per-applicant correct decision rate
Capital budget0.25Full credit if under budget; proportional if over
Risk cap0.20Full credit if ≀2 high-DTI approvals
Interest rate accuracy0.10For correctly approved applicants
Fraud safety bonus0.05All mandatory-escalation cases caught

</details>

<details> <summary><b>Task 3 β€” Hard (click to expand)</b></summary>

ComponentWeightCriteria
Decision accuracy0.30Per-applicant correct decision rate
Escalation F10.30Precision + recall on escalate class
Risk flag recall0.20Average flag recall across all applicants
Safety0.10Penalises false approvals of hard-reject cases
Fraud detection0.10Recall on fraud/unverified/prior-default

</details>


πŸ“Š Baseline Scores

Measured with gpt-4o, seed=42, temperature=0
TaskScoreNotes
task_1_easy0.85 – 0.95Occasional rate rounding errors
task_2_medium0.75 – 0.88Budget constraint requires portfolio optimisation
task_3_hard0.65 – 0.80Edge cases challenge even frontier models
Mean`0.75 – 0.87`
🎲 Random agent: ~0.10–0.30 &nbsp;&nbsp;|&nbsp;&nbsp; πŸ“ˆ All-escalate agent: ~0.45–0.65

πŸ”­ Observation Space

Returned by POST /reset and GET /state:

json
{
  "task_id": "task_1_easy",
  "step": 0,
  "max_steps": 1,
  "done": false,
  "message": "Episode started. Submit decisions for all applicants in context.applicants.",
  "context": {
    "applicants": [
      {
        "applicant_id": "APP-4821-00",
        "annual_income": 95000,
        "monthly_debt": 1200,
        "credit_score": 710,
        "loan_amount": 320000,
        "property_value": 400000,
        "employment_years": 4.5,
        "employment_type": "salaried",
        "prior_default": false,
        "fraud_flag": false,
        "income_verified": true
      }
    ],
    "policy": "ESCALATE if fraud_flag OR income_verified=false OR prior_default OR 0.40<=DTI<=0.45..."
  }
}
FieldTypeDescription
applicant_idstringUnique ID β€” copy exactly in your decision
annual_incomefloatAnnual income in USD
monthly_debtfloatTotal monthly debt obligations
credit_scoreintFICO score (580–800 range)
loan_amountfloatRequested loan amount
property_valuefloatAppraised property value
employment_yearsfloatYears at current employer
employment_typestringsalaried, self_employed, or contract
prior_defaultboolPrior loan default on record
fraud_flagboolFraud indicator triggered
income_verifiedboolIncome documentation verified

⚑ Action Space

Submitted to POST /step:

json
{
  "task_id": "task_1_easy",
  "decisions": [
    {
      "applicant_id": "APP-4821-00",
      "decision": "approve",
      "interest_rate": 7.18,
      "risk_flags": ["high_ltv"],
      "reasoning": "DTI=0.1516 LTV=0.8000 => approve. Rate=6.60."
    }
  ]
}
FieldTypeDescription
applicant_idstringMust match exactly from observation
decisionstringapprove, reject, or escalate
interest_rate`float \null`Required for approve; must be null otherwise
risk_flagsstring[]All applicable flags (see below)
reasoningstringStep-by-step justification

Risk Flags

FlagTrigger Condition
high_dtiDTI > 0.36
low_credit_scorecredit_score < 680
high_ltvLTV > 0.80
short_employmentemployment_years < 2.0
self_employed_incomeemploymenttype == `selfemployed`
prior_defaultprior_default == true
fraud_flagfraud_flag == true
unverified_incomeincome_verified == false

πŸ“ Underwriting Rules

DTI = (monthly_debt Γ— 12) / annual_income
LTV = loan_amount / property_value

━━━ Priority 1 β€” ESCALATE (human review required) ━━━
  β€’ fraud_flag = true
  β€’ income_verified = false
  β€’ prior_default = true
  β€’ 0.40 ≀ DTI ≀ 0.45  (borderline zone)

━━━ Priority 2 β€” REJECT (if not escalating) ━━━
  β€’ DTI > 0.45
  β€’ credit_score < 620
  β€’ LTV > 0.97

━━━ Priority 3 β€” APPROVE (all other cases) ━━━
  interest_rate = round(6.5 + max(0, (DTIβˆ’0.28)Γ—4) + max(0, (720βˆ’credit_score)Γ—0.01), 2)

πŸš€ Quick Start

Prerequisites

  • β€”Python 3.11+
  • β€”Docker
  • β€”API key for any OpenAI-compatible provider (OpenAI, Groq, Together AI, etc.)

1. Clone & Install

bash
git clone https://huggingface.co/spaces/bhattyuvraj22/loan-underwriting-env
cd loan-underwriting-env
pip install -r requirements.txt

2. Start the Server

bash
uvicorn main:app --host 0.0.0.0 --port 7860 --reload

3. Verify It's Running

bash
curl http://localhost:7860/health
# {"status":"ok","env":"loan-underwriting-env","version":"1.0.0"}

curl http://localhost:7860/tasks
# Lists all 3 tasks

4. Run Baseline Inference

With Groq (free tier available):

bash
export HF_TOKEN=gsk_your_groq_key
export API_BASE_URL=https://api.groq.com/openai/v1
export MODEL_NAME=llama-3.3-70b-versatile
export ENV_URL=http://localhost:7860

python inference.py

With OpenAI:

bash
export HF_TOKEN=sk-your_openai_key
export API_BASE_URL=https://api.openai.com/v1
export MODEL_NAME=gpt-4o
export ENV_URL=http://localhost:7860

python inference.py

For reproducibility:

bash
python inference.py --seed 123

🐳 Docker

bash
# Build
docker build -t loan-underwriting-env .

# Run
docker run -p 7860:7860 loan-underwriting-env

# Verify
curl http://localhost:7860/health

# Run inference against the container
HF_TOKEN=your_key \
API_BASE_URL=https://api.groq.com/openai/v1 \
MODEL_NAME=llama-3.3-70b-versatile \
ENV_URL=http://localhost:7860 \
python inference.py

🌐 API Reference

MethodEndpointDescription
GET/Root health check β€” required for HF Space ping
GET/healthHealth check
GET/tasksList all tasks with metadata
POST/resetStart new episode, returns Observation
POST/stepSubmit decisions, returns reward + info
GET/state?task_id=...Get current episode state
πŸ“– Interactive docs available at http://localhost:7860/docs

Full Episode β€” curl Example

bash
# Step 1: Reset
curl -s -X POST http://localhost:7860/reset \
  -H "Content-Type: application/json" \
  -d '{"task_id": "task_1_easy", "seed": 42}' | python -m json.tool

# Step 2: Submit decisions (copy applicant_id from reset response)
curl -s -X POST http://localhost:7860/step \
  -H "Content-Type: application/json" \
  -d '{
    "task_id": "task_1_easy",
    "decisions": [{
      "applicant_id": "APP-XXXX-00",
      "decision": "approve",
      "interest_rate": 7.18,
      "risk_flags": ["high_ltv"],
      "reasoning": "DTI=0.15 LTV=0.80 => approve"
    }]
  }' | python -m json.tool

πŸ—οΈ Project Structure

loan-underwriting-env/
β”‚
β”œβ”€β”€ πŸ“„ main.py              # FastAPI app β€” all HTTP endpoints
β”œβ”€β”€ πŸ“„ inference.py         # Baseline inference script (OpenAI-compatible)
β”œβ”€β”€ πŸ“„ openenv.yaml         # OpenEnv spec β€” observation/action space definitions
β”œβ”€β”€ πŸ“„ requirements.txt     # Python dependencies
β”œβ”€β”€ πŸ“„ pyproject.toml       # Project metadata + entry points
β”œβ”€β”€ πŸ“„ Dockerfile           # Container definition
β”œβ”€β”€ πŸ“„ uv.lock              # Locked dependency tree
β”‚
β”œβ”€β”€ πŸ“ env/
β”‚   β”œβ”€β”€ models.py           # Typed Pydantic models (Observation, AgentAction, Reward)
β”‚   β”œβ”€β”€ state.py            # Session manager (reset / step / state logic)
β”‚   β”œβ”€β”€ underwriting.py     # Applicant generator + ground truth computation
β”‚   └── graders/
β”‚       β”œβ”€β”€ grader1.py      # Easy β€” decision + rate + flags
β”‚       β”œβ”€β”€ grader2.py      # Medium β€” batch + constraints + safety
β”‚       └── grader3.py      # Hard β€” escalation F1 + fraud detection
β”‚
└── πŸ“ server/
    └── app.py              # Entry point for multi-mode deployment

🧠 Reward Design

Partial progress signal β€” Every scoring component is independent. An agent that gets decisions right but misses risk flags still earns 0.50–0.75, not zero. This gives a meaningful learning signal at every skill level.

Anti-trivial-strategy design β€” A lazy "escalate everything" strategy scores only ~0.45–0.65 because decision accuracy penalises incorrect escalations of should-approve applicants. A correct agent scores 0.90+.

Safety penalties β€” False approvals of hard-reject and fraud applicants incur explicit deductions on top of the decision accuracy loss, strongly incentivising conservative handling of risky cases.


πŸ“‹ Environment Checklist

RequirementStatus
Real-world task simulationβœ… Mortgage underwriting
OpenEnv spec compliant (step, reset, state)βœ…
3+ tasks with graders (easy β†’ hard)βœ…
Scores 0.0 – 1.0 with partial creditβœ…
Baseline inference script (inference.py)βœ…
HF Space deploys + returns 200βœ…
Dockerfile builds and runsβœ…
openenv validate passesβœ…

<div align="center">

Built for the OpenEnv Challenge Β· Powered by FastAPI Β· Hosted on Hugging Face Spaces

</div>