{"slug": "learning-deep-agents-by-building-a-claim-processing-agent-applied-ai-3", "title": "Learning Deep Agents by building a Claim Processing Agent — Applied AI #3", "summary": "LangChain's open-source Deep Agents library, built on LangChain and running on the LangGraph runtime, provides an agent harness that adds subagents, a file system and planning to the standard tool-calling loop, and a tutorial demonstrates it by building an expense claim processing agent for a fictional company, Acme Inc. The tutorial's agent uses two custom tools — read_pdf, which extracts receipt text via pdfplumber, and calculate, a restricted arithmetic evaluator — alongside Deep Agents' default file tools (ls, read_file, write_file, glob, grep), and runs on Google's gemini-3.5-flash model. The sample code and data, including claim requests, employee records, reimbursed claim history and Acme's expense policy, are published in the GitHub repository akshay-r13/claims-processing-deep-agent.", "body_md": "Some agent tasks are too big for one loop. The agent has to plan, read a lot of material, keep intermediate results somewhere, and not lose track along the way.\n\nIf you’ve used Claude Code or Claude Cowork, you’ve seen this work: you hand over a broad objective, the agent plans it, executes, and adjusts the plan as it learns more.\n\n[Deep Agents](https://www.langchain.com/deep-agents) is LangChain’s open-source library that brings this pattern to your own agents, on any model. It’s built on LangChain and runs on the LangGraph runtime, so everything you know about LangGraph still applies.\n\nLangChain calls it an *agent harness*: the usual tool-calling loop, plus built-in capabilities around it:\n\nThese sound abstract until you hit the problem they solve. So let’s hit it.\n\nEvery company has an expense reimbursement process. Employees pay out of their pocket on work-related things and submit an expense claims to get it back. Typically this includes: **Business Travel, Lodging, Meals, Office Supplies, Courses, Certifications etc.**\n\nEach claim comes with supporting documents such as receipts, invoices and tickets. HR or Finance reviews every claim against these documents and decide an action.\n\nWe’ll build an agent that does that review. It has to:\n\nEach requirement maps to one of the capabilities above. Requirement 1 is a job for subagents, requirement 2 for the file system, and requirement 3 for planning. With plain LangChain and LangGraph, you’d hand-build all of that plumbing before writing a line of claims logic. Here it comes with the library.\n\nBy the end, you’ll have a working agent that reads receipts, investigates each claim, and writes a report for a human reviewer.\n\nLet’s build it.\n\nFor this post I created sample receipts, claim requests, claim history, and a company policy for a fictional company, Acme Inc.\n\nLink to the Github repo — [https://github.com/akshay-r13/claims-processing-deep-agent](https://github.com/akshay-r13/claims-processing-deep-agent)\n\nFollowing is the repo layout:\n\n```\nclaims-processing-deep-agent/├── config/paths.py        # DATA_DIR, CLAIM_REQUESTS_LIST├── data/│   ├── claim_policy_docs/acme_expense_policy.md│   ├── claim_requests/claim_requests.csv         # new claim requests│   ├── employees/employees.csv│   ├── claim_history/reimbursed_claims.csv│   └── claim_requests/submitted_documents/CLM-1001/*.pdf├── agent/│   ├── __init__.py│   ├── tools.py│   ├── subagents.py│   ├── agent.py├── process_claim.py└── .env\n```\n\nInstall the dependencies and set your Gemini key:\n\n```\npip install deepagents langchain-google-genai pdfplumber python-dotenv\n```\n\n.env\n\n```\nGOOGLE_API_KEY=your-keyCLAIMS_MODEL=google_genai:gemini-3.5-flash\n```\n\nThe agent has four parts: tools, a subagent, the main agent, and a runner.\n\nDeep Agents come with default file tools (ls, read_file, write_file, glob, grep) that work on text. Our receipts are PDFs, and the agent can’t run code, so we add two tools.\n\nread_pdf extracts a receipt’s text. calculate is a restricted arithmetic evaluator. Models make arithmetic mistakes on tax, tip, and caps, so every sum goes through this tool.\n\n``` python\nimport astimport operatorfrom pathlib import Pathfrom config.paths import DATA_DIRimport pdfplumberfrom langchain_core.tools import tool@tooldef read_pdf(path: str) -> str:    \"\"\"Extract the text of one PDF under /data    e.g. /data/claims/CLM-1004/receipts/meal_2026-10-14_dinner.pdf. Returns plain text.\"\"\"    try:        full = resolve_data_path(path)    except ValueError as e:        return f\"ERROR: {e}\"    if not full.is_file() or full.suffix.lower() != \".pdf\":        return f\"ERROR: not a PDF file: {path}\"    with pdfplumber.open(full) as pdf:        text = \"\\n\".join(page.extract_text() or \"\" for page in pdf.pages)    return text.strip() or \"ERROR: no extractable text (scanned image? needs OCR or a vision model)\"@tooldef calculate(expression: str) -> str:    \"\"\"Evaluate a python arithmetic expression.    Supports + - * / parentheses and min(), max(), round().    Use it for EVERY sum, percentage, cap and comparison; never do arithmetic yourself.    Example: min(302.40, 6 * 50)\"\"\"    try:        result = eval(ast.parse(expression, mode=\"eval\").body)    except Exception as e:        return f\"ERROR: {e}\"    return f\"{round(result, 2):.2f}\"def resolve_data_path(virtual_path: str) -> Path:    \"\"\"Map agent path under /data/ to real file.    Reject anything that escapes the data directory.\"\"\"    rel = virtual_path.removeprefix(\"/data\").lstrip(\"/\")    full = (DATA_DIR / rel).resolve()    root = DATA_DIR.resolve()    if full != root and root not in full.parents:        raise ValueError(f\"Path outside data directory: {virtual_path}\")    return fulldef eval(node):    SUPPORTED_OPERATIONS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv}    SUPPORTED_FUNCTIONS = {\"min\": min, \"max\": max, \"round\": round}    # Plain numeric value, base condition    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):        return node.value    # + , - , x and / operations    if isinstance(node, ast.BinOp) and type(node.op) in SUPPORTED_OPERATIONS:        return SUPPORTED_OPERATIONS[type(node.op)](eval(node.left), eval(node.right))    # negative inversion    if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):        return -eval(node.operand)    # functions min max and round    if (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)            and node.func.id in SUPPORTED_FUNCTIONS and not node.keywords):        return SUPPORTED_FUNCTIONS[node.func.id](*[eval(a) for a in node.args])    raise ValueError(\"unsupported expression\")\n```\n\ncalculate is not a raw eval. It only accepts numbers, four operators, and min, max, and round. resolve_data_path keeps the agent inside the data folder.\n\nThis step solves the context-window problem. A 3-day trip has 14 receipts, and putting all their raw text into one agent’s context would bury it. Instead, each receipt goes to a subagent that reads it in an isolated context, saves the extracted fields as JSON to the file system, and returns a single line.\n\n``` python\nfrom .tools import calculate, read_pdfRECEIPT_READER = {    \"name\": \"receipt-reader\",    \"description\": (        \"Reads ONE receipt PDF and saves its extracted fields as JSON. \"        \"Give it the receipt path and the output path. \"        \"It returns a one-line summary, not the receipt text.\"    ),    \"system_prompt\": \"\"\"You extract data from one receipt PDF.        1. Call read_pdf on the path you are given.        2. Build a JSON object with: file, vendor, receipt_no, date, line_items        (list of {description, amount}), subtotal, tax, tip, total,        payment_method, notes. Put anything unusual in notes (mismatched totals,        'copy' markers, missing fields). Use null for absent fields. Do not invent values.        3. Use calculate to check that line items, tax and tip add up to the total.        Record any mismatch in notes.        4. Save the JSON with write_file to the output path you were given.        5. Reply with ONE line: vendor | date | total | output path.        Never return the raw receipt text.\"\"\",    \"tools\": [read_pdf, calculate],}\n```\n\nA subagent is simply a dictionary with a name, a description, a system prompt, and its own tools. The main agent reads the description to decide when to delegate. The “reply with ONE line” rule is the important part, because a subagent that returned everything it read would defeat the purpose.\n\nThe main agent needs a file backend, a system prompt, and the wiring.\n\nHere is the design choice that matters most. A CompositeBackend routes by path: anything under /data/ and /reports/goes to a real folder on disk, and everything else (/work) goes to an in-memory state backend. The agent’s scratch files never touch our repo.\n\n``` python\nimport osfrom deepagents import create_deep_agentfrom deepagents.backends import CompositeBackend, FilesystemBackend, StateBackendfrom .subagents import RECEIPT_READERfrom .tools import calculate, read_pdffrom config.paths import DATA_DIR, OUTPUT_DIRfrom langchain.chat_models import init_chat_modelMODEL = init_chat_model(os.environ[\"CLAIMS_MODEL\"], include_thoughts=True)SYSTEM_PROMPT = \"\"\"You are an expense claims reviewer at Acme Inc. You review one claim at a timeagainst the company policy and its receipts, then decide what happens next.Files:- /data is READ-ONLY reference data: the policy (markdown), employees.csv, the reimbursement  history CSV, and the receipt PDFs for each claim. Use ls and glob to find what you need.- /work is your scratch space. /reports is where the final report goes. Write only there.Workflow:1. Make a plan with write_todos before doing anything else.2. Read the policy in full. Look up the employee and any prior reimbursements in the CSVs.3. List the claim's receipt PDFs. Delegate each receipt to the receipt-reader subagent, one   receipt per task call, and run them in parallel. Each saves its fields to   /work/<claim_id>/extracted/<receipt-file>.json.4. Read the extracted JSON files. Cross-check them against the claim form: totals, dates,   attendees, travel request. Check for duplicates against the history and within the claim.5. Apply the policy category by category (by day for trips). Use the calculate tool for every   sum, cap and comparison. Log all key calculations under /work/<claim_id>/calc/calculations.json6. Decide: approve (in full or adjusted), reject, or request_info. If information needed for a   decision is missing, request it. Never guess. Cite policy section numbers for every   deduction or rejection. Flag claims that need sign-off.7. Write /reports/<claim_id>.md for the human reviewer, starting with these two lines:   DECISION: <approve | reject | request_info>   APPROVED_AMOUNT: <number, or n/a>   Then: a summary, a table of amounts (claimed, deducted, approved), findings with policy   references, and follow-ups (sign-off needed, questions for the employee).Finish with a one-line summary of the decision.\"\"\"def build_agent():    backend = CompositeBackend(        default=StateBackend(),        routes={\"/data/\": FilesystemBackend(root_dir=DATA_DIR, virtual_mode=True),                \"/reports/\": FilesystemBackend(root_dir=OUTPUT_DIR, virtual_mode=True)}    )    return create_deep_agent(        model=MODEL,        system_prompt=SYSTEM_PROMPT,        tools=[read_pdf, calculate],        subagents=[RECEIPT_READER],        backend=backend    )\n```\n\n**You don’t have to teach the model how to use planning, files, or subagents.** Deep Agents already includes that guidance in its built-in system prompt, and ours is appended to it. Ours only says what the job is and what the steps are.\n\nThe prompt also contains a rule that isn’t enforced: “/data is READ-ONLY”. It’s an instruction, not a permission. I come back to this at the end.\n\nThe claim form (employee, amount claimed, attendees, travel request ID) arrives as the request, the way a portal would send it. The script looks up the claim by ID and hands it to the agent:\n\n``` python\nfrom dotenv import load_dotenvload_dotenv()  # must run before the agent module reads CLAIMS_MODELimport argparseimport csvfrom agent.claims_agent import claims_agentfrom config.paths import CLAIM_REQUESTS_LISTparser = argparse.ArgumentParser()parser.add_argument(\"claim_id\")def load_claim(claim_id):    with open(CLAIM_REQUESTS_LIST, newline=\"\") as f:        for row in csv.DictReader(f):            if row[\"claim_id\"] == claim_id:                return row    raise SystemExit(f\"Unknown claim: {claim_id}\")if __name__ == \"__main__\":    args = parser.parse_args()    claims_agent.process_claim(load_claim(args.claim_id))\n```\n\nAnd the wrapper that builds the prompt and invokes the agent:\n\n``` python\nfrom .agent import build_agentclass ClaimsAgent:    def __init__(self):        self.agent = build_agent()    def process_claim(self, claim: dict):        details = \"\\n\".join(f\"{k} - {v or '(none)'}\" for k, v in claim.items())        prompt = f\"Review this expense claim.\\n---\\n{details}\\n---\"        result = self.agent.invoke({\"messages\": [{\"role\": \"user\", \"content\": prompt}]})        return resultclaims_agent = ClaimsAgent()\n```\n\nI’ll use a business trip claim request because it’s the hardest case.\n\nIt has **14 receipts**, a hotel booking with charges that aren’t reimbursable, a meal day over the cap, and a claim total that doesn’t match the receipts.\n\nHere is what to look for, and what the correct answer is:\n\nAcross all six claims:\n\nResults come out as expected.\n\nBack to the three requirements from the start:\n\nWe wrote about 150 lines of agent code, two tools, one subagent, and a prompt. Planning, context offloading, and subagent orchestration came from the library.\n\nThank you for making it to the end! As an AI Engineer I find most AI articles a bit too theoretical. I started writing these pieces to share practical, hands-on techniques you can actually apply in your day-to-day work, focusing on one concept with a working example, and code you can run yourself.\n\nFollow me for more updates 🙂, and check out the Github repository here:\n\n[Learning Deep Agents by building a Claim Processing Agent — Applied AI #3](https://pub.towardsai.net/learning-deep-agents-by-building-a-claim-processing-agent-applied-ai-3-5e98f25b73e1) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.", "url": "https://wpnews.pro/news/learning-deep-agents-by-building-a-claim-processing-agent-applied-ai-3", "canonical_source": "https://pub.towardsai.net/learning-deep-agents-by-building-a-claim-processing-agent-applied-ai-3-5e98f25b73e1?source=rss----98111c9905da---4", "published_at": "2026-11-10 17:01:02+00:00", "updated_at": "2026-11-10 17:26:23.007818+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "large-language-models", "developer-tools"], "entities": ["LangChain", "Deep Agents", "LangGraph", "Acme Inc.", "Google", "gemini-3.5-flash", "pdfplumber", "Claude Code"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/learning-deep-agents-by-building-a-claim-processing-agent-applied-ai-3", "markdown": "https://wpnews.pro/news/learning-deep-agents-by-building-a-claim-processing-agent-applied-ai-3.md", "text": "https://wpnews.pro/news/learning-deep-agents-by-building-a-claim-processing-agent-applied-ai-3.txt", "jsonld": "https://wpnews.pro/news/learning-deep-agents-by-building-a-claim-processing-agent-applied-ai-3.jsonld"}}