LangChain Financial Document Parsing: Indian Bank Statements in Python
Integrate Lekha with LangChain to parse Indian bank statements, salary slips, and CAS reports as structured JSON. Full Python code with tool use and agent examples.
LangChain is the most popular Python framework for building AI agents. It handles memory, retrieval, tool calling, and multi-step agent loops with minimal boilerplate. What it doesn't include is native support for Indian financial documents — bank statements that come in 28+ format variants, CAMS CAS reports, salary slips with regional payroll structures, CIBIL credit reports.
This guide shows how to add a Lekha tool to your LangChain agent so it can parse any Indian financial document and reason over structured data.
What You'll Build
A LangChain agent that:
By the end you'll have a reusable pattern you can extend into loan eligibility checks, expense summaries, portfolio analysis, and more.
Prerequisites
Step 1: Install Dependencies
pip install langchain langchain-anthropic python-dotenv httpx
Lekha is a plain REST API, so no additional SDK is needed.
Step 2: Create the Lekha Tool
In LangChain, tools are functions decorated with @tool. The docstring becomes the description the LLM uses to decide when — and how — to call the tool.
# tools/lekha.py
import base64
import os
from pathlib import Path
import httpx
from langchain_core.tools import tool
LEKHA_BASE = "https://api.lekhadev.com/v1"
@tool
def parse_financial_document(file_path: str, document_type: str = "auto") -> dict:
"""
Parse an Indian financial document (bank statement, salary slip, CAS, ITR,
Form 16, CIBIL report, GST return) and return structured JSON data.
Args:
file_path: Absolute or relative path to the PDF file on disk.
document_type: Type hint — one of: bank_statement, salary_slip, cas,
itr, form16, cibil, gst, balance_sheet, or auto (default).
Returns:
Structured dict with extracted fields: account holder, transactions,
balances, summary statistics, and more.
"""
pdf_bytes = Path(file_path).read_bytes()
file_base64 = base64.b64encode(pdf_bytes).decode()
response = httpx.post(
f"{LEKHA_BASE}/extract",
headers={"Authorization": f"Bearer {os.environ['LEKHA_API_KEY']}"},
json={"file": file_base64, "document_type": document_type},
timeout=60.0,
)
response.raise_for_status()
return response.json()["data"]
The @tool decorator handles all LangChain schema generation automatically — no manual JSON Schema required. Write the docstring carefully: it is what the LLM reads to decide whether to invoke this tool.
Step 3: Build the Agent
# agent.py
import os
from dotenv import load_dotenv
from langchain_anthropic import ChatAnthropic
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from tools.lekha import parse_financial_document
load_dotenv()
llm = ChatAnthropic(model="claude-sonnet-4-6", temperature=0)
tools = [parse_financial_document]
prompt = ChatPromptTemplate.from_messages([
(
"system",
"You are a financial document analyst specialising in Indian financial documents. "
"When the user mentions a document path, call parse_financial_document to extract "
"its data before answering. Always cite specific figures from the document. "
"Amounts are in Indian Rupees (₹) unless stated otherwise.",
),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
if __name__ == "__main__":
result = executor.invoke({
"input": (
"Analyse the bank statement at /tmp/hdfc_statement.pdf. "
"What is the average monthly credit, and are there any unusually large debits?"
)
})
print(result["output"])
Run it with your keys:
LEKHA_API_KEY=lk_live_xxx ANTHROPIC_API_KEY=sk-ant-xxx python agent.py
The agent calls parse_financial_document, receives the structured JSON, then reasons over the transaction list to answer the question — all in one executor.invoke call.
What Lekha Returns
For a bank statement, the tool returns something like this:
{
"document_type": "bank_statement",
"bank": "HDFC Bank",
"account_holder": "Rajan Mehta",
"account_number": "XXXX5678",
"period": { "from": "2025-10-01", "to": "2026-03-31" },
"opening_balance": 28500.0,
"closing_balance": 61200.0,
"transactions": [
{
"date": "2025-10-03",
"description": "NEFT/SALARY/OCT",
"debit": null,
"credit": 95000.0,
"balance": 123500.0,
"category": "salary"
}
],
"summary": {
"total_credits": 570000.0,
"total_debits": 537300.0,
"average_monthly_balance": 51800.0
}
}
All amounts are numbers (never strings), all dates are ISO 8601. Transactions are automatically categorised into: salary, rent, food, utilities, emi, investment, tax, transfer, and other. The LLM can filter, aggregate, and reason over these fields without any additional parsing logic in your code.
Step 4: Multi-Document Queries
Real fintech workflows cross-reference multiple documents. A lending agent, for example, needs both a bank statement (average monthly balance) and a salary slip (net monthly income). Just mention both paths in the prompt — the agent calls the tool once per file:
result = executor.invoke({
"input": (
"I'm applying for a home loan. "
"My latest salary slip is at /docs/salary_mar_2026.pdf and my "
"6-month bank statement is at /docs/hdfc_oct_mar_2026.pdf. "
"What is my net monthly salary, average monthly balance, and the "
"EMI-to-income ratio if I take a ₹50 lakh loan at 8.5% for 20 years?"
)
})
No code changes needed — the agent issues two tool calls, collects both results, then computes the ratio from the extracted figures.
Step 5: Add Conversation Memory
To let users ask follow-up questions about the same document without re-parsing every turn, wrap the executor with RunnableWithMessageHistory:
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
store: dict[str, ChatMessageHistory] = {}
def get_history(session_id: str) -> ChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
agent_with_history = RunnableWithMessageHistory(
executor,
get_history,
input_messages_key="input",
history_messages_key="chat_history",
)
First turn — document is parsed and its data enters the conversation history
agent_with_history.invoke(
{"input": "Parse /tmp/sbi_jan_jun.pdf and give me a brief summary."},
config={"configurable": {"session_id": "user_42"}},
)
Second turn — model already has the extracted data; no re-parse
agent_with_history.invoke(
{"input": "How much did I spend on EMI payments in March?"},
config={"configurable": {"session_id": "user_42"}},
)
The extracted JSON travels through message history, so subsequent questions are fast and don't consume another API call to Lekha.
Use Cases
Once the tool is wired up, the same agent handles a wide range of fintech queries without extra code:
| User prompt | How the agent responds |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
| "Is my income sufficient for a ₹30L car loan?" | Parses bank statement, checks average monthly salary credits against standard FOIR |
| "Summarise my mutual fund portfolio by fund house" | Parses CAS report, groups holdings by AMC |
| "What is my effective tax rate from Form 16?" | Parses Form 16, divides tax deducted by gross salary |
| "Flag any NACH return or cheque bounce transactions" | Scans description fields for "RTN", "RETURN", "BOUNCE" patterns |
| "What's my CIBIL score and total outstanding?" | Parses CIBIL report, extracts score and sum of open tradelines |
The LLM handles the reasoning; Lekha handles the extraction. Your application logic stays clean and model-agnostic.
Document Types Supported
Pass document_type="auto" and Lekha's classifier detects the format automatically — useful when users can upload any document:
FAQ
Does Lekha work with scanned PDFs, not just digital bank statements? Yes. Lekha uses vision AI rather than text-layer OCR, so it handles scanned printouts, phone-camera photos of passbooks, and encrypted net-banking PDFs with equal accuracy. Can I use OpenAI or Gemini instead of Anthropic? Yes — swapChatAnthropic for ChatOpenAI (from langchain-openai) or ChatGoogleGenerativeAI (from langchain-google-genai). The @tool decorator and create_tool_calling_agent are fully model-agnostic.
Is the document stored on Lekha's servers?
No. Lekha processes documents in memory and returns the extracted JSON. No PDF or image is persisted, keeping your integration compatible with India's DPDP Act data-minimisation requirements.
What if the bank statement is password protected?
Pass password in the request body: {"file": "...", "document_type": "auto", "password": "DOB01011990"}. Lekha decrypts the PDF server-side before extraction.
The full pattern — @tool decorator, create_tool_calling_agent, and optional RunnableWithMessageHistory — takes about 30 minutes to set up. Once running, your LangChain agent can parse and reason over any Indian financial document your users upload.