A stateful multi-agent underwriting copilot built with LangGraph that assists bank underwriters in processing loan applications end-to-end. The system orchestrates 10 specialized AI agents for document analysis, policy retrieval, fraud detection, risk scoring, and report generation — with a human-in-the-loop approval gate.
The system does not approve loans automatically. Final approval always remains with the human underwriter.
START
│
▼
[Supervisor Agent] ← validates input, creates workflow plan
│
├──────────────┬──────────────┬──────────────┐
▼ ▼ ▼ ▼
[Document [Policy [External [Fraud
Agent] Agent] Intelligence Agent]
(AlloyDB Agent]
Hybrid RAG) (Tavily)
│ │ │ │
└──────┬───────┴──────┬───────┘ │
▼ ▼ │
[Financial Analysis Agent] ◄─────────────────┘
│
▼
[Risk Scoring Agent]
│
▼
[Recommendation Agent]
│
▼
[Human Approval Node] ← interrupt() here
│
▼
[Report Agent]
│
▼
END
Parallel execution: Document, Policy, External Intelligence, and Fraud agents run concurrently.
| Layer | Technology |
|---|---|
| Agent Framework | LangGraph |
| LLM | Gemini 1.5 Pro / Flash (Vertex AI) |
| OCR / Document Extraction | Google Cloud Document AI |
| Embeddings | text-embedding-004 (Vertex AI) |
| Vector DB + Hybrid Search | AlloyDB for PostgreSQL (pgvector + ScaNN + tsvector + ai.hybrid_search()) |
| Reranker | Vertex AI Ranking API |
| Checkpointing | Firestore (LangGraph checkpointer) |
| Memory (Long-term) | Firestore |
| Document Storage | Google Cloud Storage (GCS) |
| Web Search | Tavily Search API |
| API Layer | FastAPI |
| Frontend | Streamlit |
| Monitoring | LangSmith |
Loan_Underwriter/
├── README.md
├── requirements.txt
├── .env.example
├── pytest.ini
├── docker-compose.yml # AlloyDB Omni local emulator
│
├── config/
│ └── settings.py # All GCP + API keys (from .env)
│
├── agents/
│ ├── supervisor_agent.py # Workflow orchestrator + validator
│ ├── document_agent.py # Document AI + Gemini Vision fallback
│ ├── policy_agent.py # AlloyDB Hybrid RAG
│ ├── external_intelligence_agent.py # Tavily web search
│ ├── fraud_agent.py # Fraud indicator detection
│ ├── financial_analysis_agent.py # DTI, FOIR, EMI calculation
│ ├── risk_scoring_agent.py # Composite risk score (0–100)
│ ├── recommendation_agent.py # Approve/Reject/Conditional
│ └── report_agent.py # PDF report + GCS upload
│
├── graph/
│ ├── state.py # UnderwritingState TypedDict
│ ├── graph_builder.py # LangGraph graph assembly
│ └── human_approval_node.py # interrupt() HITL node
│
├── rag/
│ ├── alloydb_store.py # AlloyDB client + hybrid_search()
│ ├── chunker.py # Semantic policy chunking
│ ├── embedder.py # Vertex AI text-embedding-004
│ └── policy_ingestion.py # PDF → AlloyDB ingestion pipeline
│
├── memory/
│ ├── firestore_checkpointer.py # LangGraph Firestore checkpointer
│ └── long_term_memory.py # Past decisions + exception patterns
│
├── tools/
│ ├── tavily_tool.py # Tavily search tool
│ ├── document_ai_tool.py # GCP Document AI extraction
│ └── report_tool.py # PDF generation (ReportLab)
│
├── api/
│ ├── main.py # FastAPI app entry point
│ ├── schemas.py # Pydantic request/response models
│ └── routes/
│ ├── applications.py # POST/GET /applications
│ ├── approvals.py # POST /applications/{id}/approve
│ └── reports.py # GET /applications/{id}/report
│
├── frontend/
│ └── app.py # Streamlit 4-page UI
│
├── data/
│ └── sample_policies/ # Sample lending policy PDFs
│
├── evaluation/
│ ├── rag_eval.py # Ragas RAG evaluation
│ └── agent_eval.py # Agent task completion metrics
│
└── tests/
├── test_agents.py
├── test_rag.py
└── test_api.py
git clone <repo>
cd Loan_Underwriter
pip install -r requirements.txtcp .env.example .env
# Edit .env with your GCP project, Tavily API key, LangSmith API key# Set USE_MOCK=true to run fully offline
export USE_MOCK=true
# Start the API
uvicorn api.main:app --reload --port 8000
# In another terminal, start the frontend
streamlit run frontend/app.pyOpen http://localhost:8501 for the Streamlit UI.
API docs: http://localhost:8000/docs
# Configure .env with real credentials
# Start AlloyDB Omni locally for development
docker-compose up -d
# Initialise AlloyDB schema
psql -h localhost -U postgres -d underwriting -f scripts/init_alloydb.sql
# Ingest sample policies
python -m rag.policy_ingestion
# Start API
uvicorn api.main:app --host 0.0.0.0 --port 8000
# Start frontend
streamlit run frontend/app.py --server.port 8501| Method | Path | Description |
|---|---|---|
POST |
/applications |
Submit application → triggers underwriting graph |
GET |
/applications/{id} |
Full application state + all agent outputs |
GET |
/applications/{id}/status |
Lightweight status poll |
POST |
/applications/{id}/approve |
Underwriter decision (resumes LangGraph) |
GET |
/applications/{id}/report |
Get signed GCS URL for PDF report |
GET |
/applications/{id}/report/download |
Download PDF directly (local mode) |
GET |
/health |
Health check |
curl -X POST http://localhost:8000/applications \
-H "Content-Type: application/json" \
-d '{
"applicant_name": "Priya Sharma",
"loan_type": "Personal Loan",
"requested_amount": 1500000,
"annual_income": 1200000,
"employer_name": "Infosys Limited",
"employment_type": "Salaried",
"existing_emi": 15000,
"loan_tenure_months": 60,
"interest_rate": 12.5
}'curl -X POST http://localhost:8000/applications/APP-XXXXXXXX/approve \
-H "Content-Type: application/json" \
-d '{
"status": "APPROVED",
"underwriter_id": "UW-001",
"notes": "Strong income profile, stable employer"
}'| Component | Weight |
|---|---|
| Financial Metrics (DTI, FOIR, stability) | 40% |
| Fraud Risk | 25% |
| Policy Compliance | 20% |
| External Signals (Tavily) | 15% |
| Score | Level |
|---|---|
| 0–30 | LOW |
| 31–60 | MEDIUM |
| 61–80 | HIGH |
| 81–100 | CRITICAL |
| Condition | Recommendation |
|---|---|
| Risk < 35, no fraud, no breaches | APPROVE |
| Risk 35–60 or minor breaches | CONDITIONAL_APPROVE |
| Risk > 60 or HIGH fraud | REJECT |
| CRITICAL risk or missing docs | ESCALATE |
- Risk score ≥ 70
- Fraud risk = HIGH
- Recommendation = ESCALATE or REJECT
- Risk level = CRITICAL
# Run all tests
pytest -v
# Run specific test suite
pytest tests/test_agents.py -v
pytest tests/test_rag.py -v
pytest tests/test_api.py -v
# Run evaluation
python evaluation/agent_eval.py
python evaluation/rag_eval.pyAlloyDB is used as the single vector + keyword search database:
-- Hybrid search: dense vector + sparse keyword → RRF fusion
SELECT * FROM ai.hybrid_search(
search_inputs => ARRAY[
jsonb_build_object(
'data_type', 'vector',
'table_name', 'policy_chunks',
'vec_column', 'embedding',
'query_vector', '[...embedding...]',
'limit', 10
),
jsonb_build_object(
'data_type', 'text',
'table_name', 'policy_chunks',
'query_text', 'maximum FOIR personal loan',
'limit', 10
)
]
);Set in .env:
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your_key
LANGCHAIN_PROJECT=loan-underwriterAll agent executions, tool calls, and graph transitions are traced in LangSmith.
# Start AlloyDB Omni container
docker-compose up -d
# Check status
docker-compose ps
# View logs
docker-compose logs -f alloydb
# Stop
docker-compose downSee .env.example for all configurable variables.
Key variables:
| Variable | Description | Default |
|---|---|---|
USE_MOCK |
Offline demo mode (no GCP) | false |
GCP_PROJECT_ID |
Google Cloud project ID | required |
TAVILY_API_KEY |
Tavily search API key | required |
LANGCHAIN_API_KEY |
LangSmith API key | optional |
ALLOYDB_DSN |
AlloyDB connection string | localhost |
GCS_BUCKET_NAME |
GCS bucket for docs/reports | optional |
This system is an AI assistant for loan underwriters. All AI recommendations are advisory only. Final loan decisions must be made by qualified human underwriters following applicable banking regulations and internal credit policies.
For Azure deployments, set CLOUD_PROVIDER=azure. The recommended Azure-native stack is:
| Capability | Azure service |
|---|---|
| LLM and embeddings | Microsoft Foundry / Azure OpenAI deployments |
| Policy RAG | Azure AI Search hybrid + semantic search |
| Document extraction | Azure AI Document Intelligence |
| Reports and uploaded documents | Azure Blob Storage |
| Application state | Azure Database for PostgreSQL |
| LangGraph checkpointing | Azure Database for PostgreSQL via langgraph-checkpoint-postgres |
| Optional cache | Azure Cache for Redis |
| Secrets | Azure Key Vault + Managed Identity |
| Observability | Azure Monitor + Application Insights |
Chat history is intentionally disabled for the default workflow. This app submits applications, runs the underwriting graph, captures human approval state, and returns reports; it does not need conversational memory unless a future underwriter chat interface is added.
Graph RAG is also not required for the initial Azure migration. Policy lookup is best served by Azure AI Search hybrid retrieval. Consider Graph RAG later only if the product needs relationship-heavy reasoning across applicants, employers, policies, exceptions, historical decisions, and connected fraud patterns.