Let's be honest: most AI agent frameworks are a tangled mess of prompt engineering, state management, and hope. You start with "this will automate our workflow!" and end up with a brittle system that falls apart when someone sneezes.
Deep Agents (from LangChain) is different. It's an orchestrator-based framework that actually makes sense. And to prove it, I built an auto loan underwriting system that coordinates document extraction, policy interpretation, and risk calculation - without descending into chaos.
Here's what we'll cover:
- Deep Agents framework - orchestrators, sub-agents, and memory (the stuff that matters)
- Real implementation - auto loan underwriting with actual code
- Box AI integration - document intelligence without the API wrestling
The deep agents mental model
Before we dive into code, here’s the framework in three concepts:
Orchestrators own the workflow
The orchestrator is your main agent. It doesn’t try to do everything — it coordinates. Think of it as a project manager who delegates to specialists rather than trying to write code, design UI, and deploy infrastructure simultaneously.
# The orchestrator's job:
# 1. Understand the request
# 2. Break it into tasks
# 3. Delegate to sub-agents
# 4. Synthesize the results
# 5. Make the final decisionThe orchestrator has its own tools (usually high-level ones like “upload reports”), but it doesn’t have access to every tool in the system. That’s intentional.
Sub-agents are specialists
Each sub-agent is laser-focused on one thing. They have:
- Isolated context — can’t see the orchestrator’s full state
- Specialized tools — only what they need for their job
- Single responsibility — extract data, calculate risk, interpret policy, etc.
Sub-agents can’t call other sub-agents. They do their work and return findings to the orchestrator. This isolation prevents the “everything talks to everything” spaghetti that kills most agent systems.
Memory is persistent and routed
Deep Agents supports composite backends — different types of storage for different needs:
CompositeBackend(
default=StateBackend(rt), # Ephemeral conversation state
routes={
"/memories/": FilesystemBackend() # Persistent decision reports
}
)Agents write to virtual paths like /memories/Sarah_Chen/risk_calculation.md, which map to actual files. This gives you an audit trail of every decision, calculation, and data extraction - critical for regulated industries.
Implementation of an auto loan underwriting
Let’s build something that matters. Auto loan underwriting requires:
- Document extraction — pull data from PDFs (applications, credit reports, pay stubs)
- Policy interpretation — understand underwriting rules and thresholds
- Risk calculation — compute DTI, LTV, identify violations
- Decision synthesis — make the approve/deny/escalate call
This is a perfect fit for Deep Agents because each step requires different expertise and tools.
Orchestrator setup
Here’s how we create the orchestrator in loan_orchestrator.py:
from deepagents import create_deep_agent
from deepagents.backends import (
CompositeBackend,
FilesystemBackend,
StateBackend,
)
def loan_orchestrator_create(applicant_name: str) -> CompiledStateGraph:
# Define three specialized sub-agents
sub_agents = [
{
"name": "box-extract-agent",
"description": "Retrieves and extracts data from loan documents in Box",
"system_prompt": BOX_EXTRACT_AGENT_INSTRUCTIONS,
"tools": [
search_loan_folder, # Find the applicant's folder
list_loan_documents, # List all files
ask_box_ai_about_loan, # Query documents with natural language
extract_structured_loan_data, # Extract specific fields
think_tool, # Strategic reflection
],
},
{
"name": "policy-agent",
"description": "Interprets underwriting policies and rules",
"system_prompt": POLICY_AGENT_INSTRUCTIONS,
"tools": [
ask_box_ai_about_loan, # Query policy documents
think_tool,
],
},
{
"name": "risk-calculation-agent",
"description": "Performs quantitative risk analysis",
"system_prompt": RISK_CALCULATION_AGENT_INSTRUCTIONS,
"tools": [
calculate, # Safe math evaluation
think_tool,
],
},
]
# Configure persistent memory
filesystem_backend = FilesystemBackend(
root_dir=str(memories_folder),
virtual_mode=True,
)
def backend(rt):
return CompositeBackend(
default=StateBackend(rt),
routes={
"/memories/": filesystem_backend,
},
)
# Create the orchestrator
agent = create_deep_agent(
model=init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0),
tools=[upload_text_file_to_box], # Orchestrator only uploads final reports
system_prompt=LOAN_ORCHESTRATOR_INSTRUCTIONS,
subagents=sub_agents,
backend=backend,
)
return agentKey points:
- Each sub-agent gets exactly the tools it needs — no more, no less
- The orchestrator only has upload_text_file_to_box() - it doesn't extract data or calculate risk
- Memory routing sends reports to /memories/ which persists to disk
- Temperature is 0.0 for deterministic decisions (critical for financial applications)
Workflow (from the Orchestrator’s prompt)
The orchestrator’s system prompt (loan_prompts.py) defines the workflow:
LOAN_ORCHESTRATOR_INSTRUCTIONS = """
You orchestrate the loan underwriting workflow by delegating to specialists.
## Workflow Steps
1. **Clean up**: Delete old files in `/memories/{applicant_name}/`
2. **Document Extraction**: Delegate to box-extract-agent
3. **Policy Retrieval**: Delegate to policy-agent for thresholds
4. **Risk Calculation**: Delegate to risk-calculation-agent for DTI/LTV
5. **Make Recommendation**: Synthesize findings, make decision
6. **Write Report**: Comprehensive report to `/memories/{applicant_name}/`
7. **Upload to Box**: Upload all reports back to applicant's folder
## Decision Framework
Your final recommendation must be one of:
- AUTO-APPROVE ✅ (zero violations, credit ≥700, DTI ≤40%)
- HUMAN REVIEW ⚠️ (1-2 minor violations, requires senior underwriter)
- ESCALATION REQUIRED 🚨 (moderate violations, regional director approval)
- AUTO-DENY 🚫 (major violations, unacceptable risk)
"""
The orchestrator doesn’t know how to extract data or calculate DTI — it just knows when to delegate and what to do with the results.
Box AI Integration
Here’s where Box AI comes in as the document intelligence layer. Instead of manually parsing PDFs and wrestling with document structure, we use Box’s AI capabilities through the box-ai-agents-toolkit.
The toolkit includes 150+ functions organized into:
- Box AI (Ask, Extract, Agent management)
- File/Folder Operations (CRUD, metadata, tags)
- Search (content search, folder location)
- Collaboration (sharing, permissions, groups)
- Enterprise Features (tasks, DocGen, metadata templates)
For this demo, we use 5 key functions wrapped as LangChain tools in loan_tools.py:
Search for loan folders
from langchain_core.tools import tool
from box_ai_agents_toolkit import box_locate_folder_by_name
@tool(parse_docstring=True)
def search_loan_folder(applicant_name: str) -> str:
"""Locate a loan application folder in Box by applicant name.
Args:
applicant_name: Name of the loan applicant (e.g., "Sarah Chen")
Returns:
Folder ID and path information
"""
folder = box_locate_folder_by_name(
client=conf.box_client,
folder_name=applicant_name,
parent_folder_id=conf.BOX_DEMO_PARENT_FOLDER,
)
if folder:
return f"Found folder: {folder.name} (ID: {folder.id})"
return f"Folder not found for applicant: {applicant_name}"The @tool decorator converts any function into a LangChain tool. The docstring becomes the tool's description that the agent sees.
List documents in a folder
from box_ai_agents_toolkit import box_folder_items_list
@tool(parse_docstring=True)
def list_loan_documents(folder_id: str) -> str:
"""List all documents in a loan application folder.
Args:
folder_id: Box folder ID containing the loan application
Returns:
List of files in the folder with names and IDs
"""
response = box_folder_items_list(
client=conf.box_client,
folder_id=folder_id,
is_recursive=False
)
result = f"Documents in folder {folder_id}:\n\n"
for item in response.get("folder_items", []):
item_type = "📁" if item.get("type") == "folder" else "📄"
result += f"{item_type} {item.get('name')} (ID: {item.get('id')})\n"
return resultAsk Box AI questions
This is where it gets interesting. Instead of parsing PDFs manually, we ask Box AI:
from box_ai_agents_toolkit import box_ai_ask_file_multi
@tool(parse_docstring=True)
def ask_box_ai_about_loan(folder_id: str, question: str) -> str:
"""Ask Box AI a question about documents in a loan application folder.
Args:
folder_id: Box folder ID containing the loan application
question: Question to ask about the loan application
Returns:
Box AI's response with information from the documents
"""
# Get all file IDs in the folder
folder_response = box_folder_items_list(
client=conf.box_client,
folder_id=folder_id,
is_recursive=False
)
file_ids = [
item.get("id")
for item in folder_response.get("folder_items", [])
if item.get("type") == "file"
]
if not file_ids:
return f"No files found in folder {folder_id}"
# Ask Box AI about the files
ai_response = box_ai_ask_file_multi(
client=conf.box_client,
file_ids=file_ids,
prompt=question
)
# Format the response
result = f"Box AI Response for: {question}\n\n"
if isinstance(ai_response, dict):
answer = ai_response.get("AI_response", {}).get("answer", "")
result += f"Answer: {answer}\n"
return resultExample queries the agent makes:
- “What is the applicant’s monthly gross income?”
- “What is the maximum DTI ratio allowed for standard approval?”
- “Does the applicant have any recent repossessions?”
Box AI reads the PDFs, extracts the answer, and provides citations. No PDF parsing libraries, no regex hell.
Extract structured data
For structured extraction (credit score, income, employment details), we use Box AI Extract:
from box_ai_agents_toolkit import box_ai_extract_structured_enhanced_using_fields
@tool(parse_docstring=True)
def extract_structured_loan_data(folder_id: str, fields_schema: str) -> str:
"""Extract structured data from loan application documents using Box AI Extract.
Args:
folder_id: Box folder ID containing the loan application
fields_schema: JSON string defining fields to extract
Returns:
Extracted structured data from the documents
"""
# Get file IDs
folder_response = box_folder_items_list(
client=conf.box_client,
folder_id=folder_id,
is_recursive=False
)
file_ids = [
item.get("id")
for item in folder_response.get("folder_items", [])
if item.get("type") == "file"
]
# Parse the fields schema
fields = json.loads(fields_schema)
# Extract structured data using Box AI
ai_response = box_ai_extract_structured_enhanced_using_fields(
client=conf.box_client,
file_ids=file_ids,
fields=fields
)
result = f"Extracted Data from Folder {folder_id}:\n\n"
result += json.dumps(ai_response.get("AI_response", {}).get("answer", {}), indent=2)
return result
The agent defines fields like:
[
{
"key": "credit_score",
"type": "number",
"prompt": "Applicant's FICO credit score"
},
{
"key": "monthly_income",
"type": "float",
"prompt": "Gross monthly income in dollars"
}
]Box AI extracts the values across multiple documents and returns structured JSON. No manual field mapping.
Upload reports back to Box
After making a decision, the orchestrator uploads reports:
from utils.box_api_generic import local_file_upload
@tool(parse_docstring=True)
def upload_text_file_to_box(
parent_folder_id: str,
file_name: str,
local_file_path: Path
) -> str:
"""Upload a text file to a specified Box folder.
Args:
parent_folder_id: Box folder ID where the file will be uploaded
file_name: Name of the file to be created in Box
local_file_path: Path to the local text file to upload
Returns:
Confirmation message with uploaded file details
"""
# Translate virtual path to real filesystem path
real_file_path = conf.local_agents_memory / local_file_path.relative_to("/memories/")
file_id = local_file_upload(
client=conf.box_client,
local_file_path=real_file_path,
parent_folder_id=parent_folder_id,
)
return f"File '{file_name}' uploaded successfully (File ID: {file_id})"This closes the loop: extract from Box, process with agents, upload results back to Box.
Running the demo
The entry point is demo_loan.py:
import asyncio
from agents.loan_orchestrator import loan_orchestrator_create
from utils.display_messages import stream_agent
async def test_loan_application(applicant_name: str):
# Create the orchestrator
agent = loan_orchestrator_create(applicant_name=applicant_name)
# Process the loan application
request = f"Please process the auto loan application for {applicant_name}"
# Stream the response (real-time updates)
await stream_agent(
agent,
{"messages": [{"role": "user", "content": request}]}
)
# Run it
asyncio.run(test_loan_application("David Martinez"))
What happens:
Orchestrator receives request: “Process loan for David Martinez”
Delegates to box-extract-agent:
- Searches for “David Martinez” folder
- Lists documents
- Extracts structured data (income, credit score, vehicle details)
- Writes findings to /memories/David_Martinez/data_extraction.md
Delegates to policy-agent:
- Queries policy documents for DTI/credit thresholds
- Writes findings to /memories/David_Martinez/policy.md
Delegates to risk-calculation-agent:
- Calculates DTI: (monthly_debts + proposed_payment) / monthly_income
- Calculates LTV: loan_amount / vehicle_value
- Identifies violations
- Writes findings to /memories/David_Martinez/risk_calculation.md
Orchestrator synthesizes:
- Reviews all sub-agent reports
- Applies decision framework
- Writes final decision to /memories/David_Martinez/underwriting_decision.md
Orchestrator uploads: All reports back to David’s Box folder
David Martinez’s outcome: 🚨 ESCALATION REQUIRED
- Credit: 640 (fair)
- DTI: 47% (moderate violation, >43%)
- LTV: 107% (minor violation, negative equity)
- Decision: Requires regional director approval
Why this works
Separation of concerns
Each agent has one job:
- box-extract-agent doesn’t calculate risk
- policy-agent doesn’t extract data
- risk-calculation-agent doesn’t interpret policies
This makes the system debuggable. If the DTI calculation is wrong, you know exactly where to look.
Persistent audit trail
Every calculation, extraction, and decision is written to /memories/. For regulated industries, this is gold. You can trace back:
- Where did the credit score come from? → data_extraction.md
- How was DTI calculated? → risk_calculation.md
- What policy threshold was used? → policy.md
Tool isolation
The orchestrator can’t accidentally call calculate() - it doesn't have access to that tool. This prevents the "God agent" anti-pattern where one agent tries to do everything.
Box AI does the heavy lifting
Instead of:
# Manual PDF parsing nightmare
pdf_text = extract_text_from_pdf(file_path)
income = parse_income_from_text(pdf_text) # Good luck with this
credit_score = parse_credit_score(pdf_text) # Hope the format doesn't change
You get:
# Natural language query
answer = ask_box_ai_about_loan(folder_id, "What is the applicant's monthly income?")Box AI handles OCR, document understanding, and context extraction. You focus on business logic.
The demo spectrum
The repo includes 4 test applicants representing the full decision spectrum:
Run all four to see how the agent handles different risk levels:
asyncio.run(test_loan_application("Sarah Chen")) # Clean approval
asyncio.run(test_loan_application("Jennifer Lopez")) # Clear denial
Key takeaways
Deep Agents gives you:
- Orchestrator pattern — coordinate workflows without chaos
- Sub-agent isolation — single responsibility, limited tool access
- Persistent memory — audit trails for compliance
- Type-safe tools — LangChain tool system with validation
Box AI gives you:
- Natural language document queries — no PDF parsing
- Structured data extraction — define fields, get JSON
- 150+ toolkit functions — file ops, search, collaboration, enterprise features
- Enterprise-ready — security, compliance, audit logs
Together:
- Build multi-agent systems that don’t collapse under their own complexity
- Process documents intelligently without reinventing OCR
- Create audit trails for regulated workflows
- Focus on business logic, not infrastructure
Try it yourself
Repo: github.com/box-community/langchain-box-loan-demo
Requirements:
- Python 3.13+
- Box account (free tier works)
- Anthropic API key
Questions? Open an issue or reach out. I’m a developer advocate at Box, not a salesperson — I want to see what you build with this.

