Building systems with Langchain deep agents and Box

|
Share

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 decision

The 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:

  1. Document extraction — pull data from PDFs (applications, credit reports, pay stubs)
  2. Policy interpretation — understand underwriting rules and thresholds
  3. Risk calculation — compute DTI, LTV, identify violations
  4. 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 agent

Key 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 result

Ask 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 result

Example 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:

Applicant

Credit

DTI

Outcome

Sarah Chen

750

12.1%

✅ AUTO-APPROVE

Marcus Johnson

680

42.1%

⚠️ HUMAN REVIEW

David Martinez

640

47.0%

🚨 ESCALATION REQUIRED

Jennifer Lopez

575

54.7%

🚫 AUTO-DENY

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.