smolagents + Lekha: Financial Document AI Agents
Build Indian financial document AI agents with HuggingFace smolagents and Lekha API. Parse bank statements, salary slips, and ITR with Python in minutes.
CodeAgent, a mode where the LLM writes and executes Python code rather than emitting JSON tool calls. When you wire it up to Lekha, you get a document-aware financial agent that can parse Indian bank statements, salary slips, ITR, and CAS reports — and reason over them in code.
This guide walks you through building a complete financial document intelligence agent with smolagents and Lekha, from a simple one-shot extraction to a multi-document loan underwriting pipeline.
Why smolagents for Financial Document AI?
smolagents takes a different approach than LangChain or CrewAI. Instead of chaining prompt templates, it lets the LLM author runnable Python snippets. For financial analysis, this is powerful: the model can write arithmetic, aggregations, and conditional logic over extracted data rather than describing them in natural language.
What you get with smolagents + Lekha:CodeAgent handles the reasoning — writing Python to compute savings rates, debt ratios, and income trendsPrerequisites
Install the packages:
pip install smolagents httpx python-dotenv
Set your API key:
export LEKHA_API_KEY="lk_live_your_key_here"
Get a free key at lekhadev.com. No credit card required.
Step 1: Wrap Lekha as a smolagents Tool
smolagents tools are plain Python classes that inherit from Tool. Define a LekhaExtractTool that calls the Lekha API and returns structured JSON.
import os
import httpx
from smolagents import Tool
LEKHA_API_KEY = os.environ["LEKHA_API_KEY"]
LEKHA_BASE_URL = "https://api.lekhadev.com/v1"
class LekhaExtractTool(Tool):
name = "extract_financial_document"
description = (
"Extracts structured data from an Indian financial document (bank statement, "
"salary slip, ITR, CAS, Form 16, CIBIL report). "
"Pass the local file path and the document type. "
"Returns structured JSON with transactions, account details, and summary."
)
inputs = {
"file_path": {
"type": "string",
"description": "Local path to the PDF or image file",
},
"document_type": {
"type": "string",
"description": (
"One of: bank_statement, salary_slip, itr, cas_statement, "
"form_16, cibil_report, gst_return, balance_sheet"
),
},
}
output_type = "object"
def forward(self, file_path: str, document_type: str) -> dict:
with open(file_path, "rb") as f:
response = httpx.post(
f"{LEKHA_BASE_URL}/extract",
headers={"x-api-key": LEKHA_API_KEY},
files={"file": (os.path.basename(file_path), f, "application/pdf")},
data={"document_type": document_type},
timeout=60.0,
)
result = response.json()
if not result.get("success"):
raise ValueError(f"Lekha extraction failed: {result.get('error', {}).get('message')}")
return result["data"]
The forward method is what smolagents calls when the agent decides to use the tool. Lekha returns amounts as numbers and dates as ISO 8601 strings — no further cleaning needed.
Try a raw extraction first at lekhadev.com/playground to see exactly what JSON your documents produce.
Step 2: Build a ToolCallingAgent
ToolCallingAgent is smolagents' standard agent mode — the LLM emits JSON tool calls. This is the right choice for straightforward document extraction tasks.
from smolagents import ToolCallingAgent, HfApiModel
Use any HuggingFace model as the backbone
model = HfApiModel("Qwen/Qwen2.5-72B-Instruct")
agent = ToolCallingAgent(
tools=[LekhaExtractTool()],
model=model,
system_prompt="""You are an expert Indian financial analyst.
When asked about a financial document, use the extract_financial_document tool
to get structured data. Then answer the user's question using the extracted data.
Important context:
- Amounts are in INR, always numbers (e.g., 45000.0 = ₹45,000)
- Transaction types: credit, debit, transfer
- Common Indian salary components: basic, HRA, conveyance, special_allowance
- UPI transactions appear as: UPI///
""",
)
Ask about a bank statement
answer = agent.run(
"What is the average monthly balance and total salary credits "
"in this bank statement? File: /docs/hdfc_statement_q2_2026.pdf"
)
print(answer)
Step 3: Use CodeAgent for Complex Financial Analysis
CodeAgent is smolagents' more powerful mode — the LLM writes Python code that runs in a sandboxed interpreter. For financial analysis (ratios, trend lines, outlier detection), this produces far more accurate results than tool-calling alone.
from smolagents import CodeAgent, HfApiModel
model = HfApiModel("Qwen/Qwen2.5-72B-Instruct")
analyst = CodeAgent(
tools=[LekhaExtractTool()],
model=model,
additional_authorized_imports=["statistics", "collections", "datetime"],
)
report = analyst.run("""
Extract the bank statement at '/docs/sbi_statement_fy2026.pdf' and then:
Calculate the average monthly net cash flow (credits minus debits) for each month
Identify the top 5 largest single debit transactions
Flag any month where the closing balance dropped below 10,000
Compute the savings rate: (total credits - total debits) / total credits
Return a structured summary with all four metrics.
""")
print(report)
CodeAgent will write and execute Python like this to answer the question:
# Code the LLM generates and runs:
data = extract_financial_document("/docs/sbi_statement_fy2026.pdf", "bank_statement")
from collections import defaultdict
monthly_flows = defaultdict(lambda: {"credits": 0, "debits": 0})
for tx in data["transactions"]:
month = tx["date"][:7] # YYYY-MM
if tx.get("credit"):
monthly_flows[month]["credits"] += tx["credit"]
if tx.get("debit"):
monthly_flows[month]["debits"] += tx["debit"]
monthly_net = {
month: flows["credits"] - flows["debits"]
for month, flows in sorted(monthly_flows.items())
}
top_debits = sorted(
[tx for tx in data["transactions"] if tx.get("debit")],
key=lambda x: x["debit"],
reverse=True
)[:5]
low_balance_months = [
month for month, flows in monthly_flows.items()
# approximate: closing = opening + credits - debits
]
total_credits = sum(t.get("credit") or 0 for t in data["transactions"])
total_debits = sum(t.get("debit") or 0 for t in data["transactions"])
savings_rate = (total_credits - total_debits) / total_credits if total_credits else 0
print({"monthly_net_flow": monthly_net, "top_5_debits": top_debits, "savings_rate": savings_rate})
The agent writes the aggregation logic itself. You do not need to predefine every calculation you might want.
Step 4: Multi-Document Loan Underwriting Agent
For loan origination, you typically need to cross-reference a bank statement, salary slip, and (optionally) an ITR. smolagents handles this naturally — give the CodeAgent multiple files and ask it to reason across them.
underwriter = CodeAgent(
tools=[LekhaExtractTool()],
model=HfApiModel("Qwen/Qwen2.5-72B-Instruct"),
additional_authorized_imports=["statistics"],
system_prompt="""You are an Indian NBFC loan underwriter.
Apply RBI guidelines:
- Monthly EMI obligation must not exceed 50% of net take-home salary
- Minimum 3 months of stable income on bank statement
- No ECS/NACH returns in the last 3 months
- Salary credits on bank statement must corroborate salary slip amount (within 10%)
Always output a loan_eligible (true/false) decision with a reason.
""",
)
decision = underwriter.run("""
Evaluate this loan application:
- Bank statement: /docs/applicant_kotak_6m.pdf
- Salary slip: /docs/applicant_salaryslip_sep2026.pdf
- Requested loan amount: 500000
- Requested tenure: 36 months
- Interest rate: 12% per annum
Extract both documents and determine loan eligibility with the calculated EMI.
""")
print(decision)
The agent will extract both documents, compute the EMI (500000 0.01 / (1 - (1.01)*-36) ≈ ₹16,607), check it against the extracted salary, scan for NACH returns, and return a structured decision.
Step 5: Using Claude as the Backbone
smolagents supports Anthropic models via the LiteLLMModel adapter. Claude performs better than open-source models on complex multi-document financial reasoning.
from smolagents import CodeAgent, LiteLLMModel
model = LiteLLMModel(
model_id="anthropic/claude-opus-5-5",
api_key=os.environ["ANTHROPIC_API_KEY"],
)
analyst = CodeAgent(
tools=[LekhaExtractTool()],
model=model,
)
For production workloads, Claude is the recommended choice for accuracy on Indian financial documents. The free-tier Lekha plan works with any backbone model.
Handling Indian-Specific Edge Cases
UPI Transaction Parsing
UPI credits and debits use terse description formats: UPI/924301234567/RAHUL SHARMA/PHONEPE. Tell the CodeAgent to parse the app name and counterparty from this pattern, or pre-process them in the tool layer.
Password-Protected PDFs
Many Indian bank net banking exports are password-protected. Pass the password parameter in the Lekha API call:
response = httpx.post(
f"{LEKHA_BASE_URL}/extract",
headers={"x-api-key": LEKHA_API_KEY},
files={"file": (filename, f, "application/pdf")},
data={
"document_type": "bank_statement",
"password": "PAN_DDMMYYYY", # common Indian bank pattern
},
timeout=60.0,
)
Multi-Page Statements
Lekha processes multi-page PDFs natively — no chunking needed. A 12-month SBI statement spanning 40 pages returns all transactions in a single API response. See the multi-page PDF guide for edge cases.
Testing Your Agent
smolagents includes a mock model for unit testing:
from smolagents.testing import MockedLLM
def test_extraction_tool():
tool = LekhaExtractTool()
# Test with a real fixture file
result = tool.forward("tests/fixtures/hdfc_sample.pdf", "bank_statement")
assert result["bank"] == "HDFC Bank"
assert isinstance(result["transactions"], list)
assert all(isinstance(tx["date"], str) for tx in result["transactions"])
# Dates must be ISO 8601
assert result["transactions"][0]["date"].count("-") == 2
Keep fixture files in tests/fixtures/ and never commit real customer statements. See the full docs at lekhadev.com/docs for test helpers.
FAQ
What Indian documents does Lekha support with smolagents?Lekha supports bank statements (50+ banks), salary slips, ITR (all forms), CAS statements, Form 16, CIBIL reports, GST returns, and balance sheets. Pass the appropriate document_type to the extraction tool. The classifier also auto-detects format if you omit it.
Yes. Replace HfApiModel with LiteLLMModel(model_id="ollama_chat/qwen2.5:72b"). Lekha's free plan also routes to Ollama for the extraction step, so the entire pipeline can run locally for testing. Accuracy on smaller models will vary.
smolagents runs CodeAgent in a restricted Python interpreter by default. You control which imports are allowed via additional_authorized_imports. Financial analysis only needs statistics, collections, datetime, and math — keep the allowlist narrow.
Yes. Wrap the agent.run() call in an async def route using asyncio.to_thread. smolagents is synchronous by default, but the extraction tool (httpx.post) can be made async with httpx.AsyncClient. See the Lekha REST API docs for async patterns.
smolagents keeps the agent layer minimal and readable — ideal for fintech prototypes that need to move fast without locking into a heavy framework. Lekha handles the messy document layer, so your agent code stays focused on financial logic.
Ready to build? Sign up for a free Lekha API key at lekhadev.com and try the interactive playground to preview your documents as structured JSON before writing a single line of agent code.