Agno + Lekha: Financial Document Agents in Python
Build Indian financial document AI agents with Agno (formerly phidata) and Lekha API. Parse bank statements, salary slips, and ITR with agent teams in Python.
This guide builds a complete financial document intelligence system using Agno's agent team pattern: a document extraction agent, a financial analysis agent, and an underwriting coordinator that orchestrates both.
Why Agno for Indian Financial Document AI?
Agno takes a different philosophy from CrewAI or LangGraph. There are no chains, no flow graphs — just agents with tools, memory, and optional storage. The framework is thin enough to understand fully in an afternoon, and fast enough to use in latency-sensitive API routes.
What Agno brings to the Lekha stack:Prerequisites
Install packages:
pip install agno lekhadev httpx python-dotenv
Set your API keys:
export LEKHA_API_KEY="lk_live_your_key_here"
export ANTHROPIC_API_KEY="sk-ant-your_key_here"
Get a free Lekha key at lekhadev.com — no credit card required. Test extraction interactively at lekhadev.com/playground.
Step 1: Define Lekha as Agno Tools
Agno tools are plain Python functions with type annotations and docstrings. The framework reads the docstring as the tool description and infers parameter types from annotations.
import os
import httpx
from pathlib import Path
LEKHA_API_KEY = os.environ["LEKHA_API_KEY"]
LEKHA_BASE_URL = "https://api.lekhadev.com/v1"
def extract_bank_statement(file_path: str, password: str = "") -> dict:
"""
Extract structured data from an Indian bank statement PDF.
Returns account holder details, transactions list, and summary.
All amounts are numbers in INR. Dates are ISO 8601 (YYYY-MM-DD).
Supports all major Indian banks: HDFC, SBI, ICICI, Axis, Kotak, etc.
"""
with open(file_path, "rb") as f:
payload = {"document_type": "bank_statement"}
if password:
payload["password"] = password
response = httpx.post(
f"{LEKHA_BASE_URL}/extract",
headers={"x-api-key": LEKHA_API_KEY},
files={"file": (Path(file_path).name, f, "application/pdf")},
data=payload,
timeout=60.0,
)
result = response.json()
if not result.get("success"):
raise ValueError(f"Extraction failed: {result.get('error', {}).get('message')}")
return result["data"]
def extract_salary_slip(file_path: str) -> dict:
"""
Extract structured data from an Indian salary slip (payslip) PDF.
Returns employee details, CTC breakdown, net take-home pay, and deductions.
Supports all common Indian payroll formats including TCS, Infosys, HRMS exports.
"""
with open(file_path, "rb") as f:
response = httpx.post(
f"{LEKHA_BASE_URL}/extract",
headers={"x-api-key": LEKHA_API_KEY},
files={"file": (Path(file_path).name, f, "application/pdf")},
data={"document_type": "salary_slip"},
timeout=60.0,
)
result = response.json()
if not result.get("success"):
raise ValueError(f"Extraction failed: {result.get('error', {}).get('message')}")
return result["data"]
def extract_itr_document(file_path: str) -> dict:
"""
Extract structured data from an Indian ITR (Income Tax Return) acknowledgment or computation.
Returns gross income, tax paid, refund status, and income source breakdown.
"""
with open(file_path, "rb") as f:
response = httpx.post(
f"{LEKHA_BASE_URL}/extract",
headers={"x-api-key": LEKHA_API_KEY},
files={"file": (Path(file_path).name, f, "application/pdf")},
data={"document_type": "itr"},
timeout=60.0,
)
result = response.json()
if not result.get("success"):
raise ValueError(f"Extraction failed: {result.get('error', {}).get('message')}")
return result["data"]
Notice there is no base class, no decorator, no registration step. Agno reads these functions directly — you pass them in the tools list when creating an agent.
Step 2: Build a Single-Document Extraction Agent
from agno.agent import Agent
from agno.models.anthropic import Claude
extractor = Agent(
name="Document Extractor",
model=Claude(id="claude-opus-5-5"),
tools=[extract_bank_statement, extract_salary_slip, extract_itr_document],
instructions="""
You are an expert in Indian financial documents.
When given a file path and document type, use the appropriate extraction tool.
Always report the extracted data in a structured way:
- Bank statement: account summary, top 5 transactions by amount, monthly credits
- Salary slip: employer, employee, gross pay, net take-home, PF deduction
- ITR: AY, gross income, total tax paid, outstanding refund
Amounts are in INR. Dates are ISO 8601.
""",
markdown=True,
)
Use it
extractor.print_response(
"Extract and summarize this bank statement: /docs/hdfc_june2026.pdf"
)
Agno agents stream responses to the terminal by default. For API use, call agent.run(message) and access response.content.
Step 3: Build a Financial Analysis Agent with Memory
Agno's memory system lets an agent remember facts across multiple turns. This is valuable for financial workflows where you extract a document in one step and ask follow-up questions later.
from agno.agent import Agent
from agno.models.anthropic import Claude
from agno.memory.v2.db.sqlite import SqliteMemoryDb
from agno.memory.v2.memory import Memory
from agno.storage.sqlite import SqliteStorage
Persist agent memory and sessions to a local SQLite database
memory = Memory(
db=SqliteMemoryDb(table_name="agent_memory", db_file="financial_agents.db"),
)
storage = SqliteStorage(table_name="agent_sessions", db_file="financial_agents.db")
analyst = Agent(
name="Financial Analyst",
model=Claude(id="claude-opus-5-5"),
tools=[extract_bank_statement, extract_salary_slip, extract_itr_document],
memory=memory,
storage=storage,
instructions="""
You are a senior financial analyst specializing in Indian personal finance.
After extracting a document, remember the key figures for follow-up questions.
When computing ratios or trends, show your working.
Indian-specific context:
- NACH/ECS returns indicate failed direct debits — a credit risk signal
- UPI transaction format: UPI///
- Common salary credit narrations: SALARY, NEFT SALARY, IMPS SALARY
- PF deduction is typically 12% of basic salary
""",
markdown=True,
add_history_to_messages=True,
num_history_runs=5,
)
Multi-turn conversation
analyst.print_response("Extract this bank statement: /docs/icici_q2_2026.pdf")
analyst.print_response("What was the highest single debit in July?")
analyst.print_response("Were there any NACH returns?")
analyst.print_response("Compute the savings rate for each month.")
Because add_history_to_messages=True, the agent carries extraction context through all four turns — you don't re-extract the document on each question.
Step 4: Agent Teams for Loan Underwriting
Agno's team pattern lets you define a coordinator agent that delegates to specialist agents. For loan underwriting, this maps naturally: one agent extracts documents, another runs financial checks, and the coordinator writes the decision.
from agno.agent import Agent
from agno.team import Team
from agno.models.anthropic import Claude
Specialist 1: Extraction
extraction_agent = Agent(
name="Extraction Specialist",
role="Extract and structure Indian financial documents using Lekha",
model=Claude(id="claude-opus-5-5"),
tools=[extract_bank_statement, extract_salary_slip, extract_itr_document],
instructions="""
Extract the requested documents and return clean structured data.
For bank statements: provide account number, period, total credits, total debits,
average balance, salary credits list, and NACH/ECS return count.
For salary slips: provide employer, employee PAN, net monthly income, PF amount.
""",
)
Specialist 2: Credit Analysis
credit_agent = Agent(
name="Credit Analyst",
role="Evaluate creditworthiness based on extracted financial data",
model=Claude(id="claude-opus-5-5"),
instructions="""
Apply RBI NBFC lending guidelines:
- EMI-to-income ratio must not exceed 50% of net take-home salary
- Require at least 3 consistent salary credits on bank statement
- Zero tolerance for NACH/ECS returns in the last 3 months
- Salary on bank statement must be within 10% of salary slip figure
- Minimum average monthly balance: 3× requested monthly EMI
Output: eligibility (yes/no), max eligible loan amount, reason.
""",
)
Coordinator: Underwriter
underwriter = Team(
name="Loan Underwriting Team",
mode="coordinate",
model=Claude(id="claude-opus-5-5"),
members=[extraction_agent, credit_agent],
instructions="""
You are the lead underwriter. For each application:
1. Ask the Extraction Specialist to process all provided documents
2. Pass the extracted data to the Credit Analyst for assessment
3. Compile a final underwriting report with: decision, max amount, risk flags
Be decisive. Use the extracted numbers, not guesses.
""",
markdown=True,
)
decision = underwriter.run("""
New loan application received:
- Bank statement (6 months): /docs/applicant_axis_6m.pdf
- Salary slip (latest): /docs/applicant_salary_sep2026.pdf
- Requested loan: ₹8,00,000 over 48 months at 13% per annum
Process both documents and make an underwriting decision.
""")
print(decision.content)
The coordinator dispatches to specialists, collects their outputs, and synthesizes a single decision — all without you manually wiring up tool calls or managing agent state.
Step 5: Structured Output with Pydantic
For production APIs, you want a typed Python object back — not a markdown string. Agno supports Pydantic response_model on any agent.
from pydantic import BaseModel
from typing import Optional
from agno.agent import Agent
from agno.models.anthropic import Claude
class LoanDecision(BaseModel):
eligible: bool
approved_amount: Optional[float]
monthly_emi: Optional[float]
debt_to_income_ratio: float
salary_verified: bool
nach_returns: int
risk_flags: list[str]
reason: str
decision_agent = Agent(
name="Underwriting Decision Agent",
model=Claude(id="claude-opus-5-5"),
tools=[extract_bank_statement, extract_salary_slip],
response_model=LoanDecision,
instructions="""
Extract the provided documents and return a structured loan decision.
Compute EMI as: principal × monthly_rate / (1 - (1 + monthly_rate)^-tenure).
""",
)
result = decision_agent.run(
"Bank: /docs/sbi_stmt.pdf — Salary: /docs/salary.pdf — "
"Loan: 500000, 36 months, 12% p.a."
)
decision: LoanDecision = result.content
print(f"Eligible: {decision.eligible}")
print(f"DTI ratio: {decision.debt_to_income_ratio:.1%}")
print(f"Risk flags: {decision.risk_flags}")
The agent's response is automatically validated against LoanDecision. Invalid responses trigger an internal retry loop (configurable with structured_output_retries).
Handling Indian-Specific Edge Cases
Password-Protected PDFs
Most Indian bank portals generate password-protected statements. Pass the PDF password to the Lekha tool:
data = extract_bank_statement("/docs/sbi_stmt.pdf", password="AADHA12345")
Common password patterns: PAN number (uppercase), date of birth (DDMMYYYY), account number last 4 digits.
Multi-Bank Borrowers
For borrowers with accounts at multiple banks, run extractions in parallel using Python's asyncio.gather or Agno's async agent variant (AsyncAgent):
import asyncio
from agno.agent import Agent
async def extract_all(file_paths: list[str]) -> list[dict]:
async with asyncio.TaskGroup() as tg:
tasks = [
tg.create_task(asyncio.to_thread(extract_bank_statement, fp))
for fp in file_paths
]
return [t.result() for t in tasks]
UPI Transaction Categorisation
UPI narrations follow a predictable format: UPI/924301234567/RAHUL SHARMA/PHONEPE. The Credit Analyst agent can parse merchant names and apps from this pattern in its reasoning step without any extra preprocessing code.
Deploying Agno Agents as an API
Agno includes a FastApiApp helper that wraps an agent or team as a REST endpoint:
from agno.app.fastapi import FastApiApp
app = FastApiApp(agent=underwriter, name="Lekha Underwriting API")
Run with: uvicorn main:app.app
The endpoint accepts { "message": "...", "session_id": "..." } and returns the agent's response. Session IDs route each borrower's multi-turn conversation to the same storage bucket.
See full deployment options in the Lekha API docs.
FAQ
Which Agno version does this guide use?This guide targets Agno v1.x (post the phidata → Agno rename). Install with pip install agno. If you are on an older phidata install, pip install agno upgrades the package; the Agent and Team APIs are compatible with minor import path changes.
Yes. Replace Claude(id="...") with Ollama(id="llama3.3") or OpenAILike(id="qwen2.5-72b-instruct", base_url="...", api_key="..."). Lekha's free plan also routes to Qwen via Ollama on the extraction side, so the full pipeline can run locally for development. Production use should use Claude for best accuracy on Indian document formats.
Agno memory is session-persistent and stored in a real database (SQLite or PostgreSQL), not held in RAM. This means an agent remembers extracted document data across HTTP requests, process restarts, and even days — useful for multi-step loan workflows where a borrower submits documents at different times.
Can I use Agno with Lekha's/classify endpoint to auto-detect document type?
Yes. Call POST /classify first (it returns the detected document_type), then route to the appropriate extraction tool. Add a classify_document(file_path) function to your tools list and instruct the agent to call it before extracting any unknown document.
Agno's team-based architecture and built-in storage make it one of the cleanest frameworks for production financial document pipelines. Lekha handles the messy document layer — 50+ Indian bank formats, password-protected PDFs, multi-page statements — so your agent code stays focused on financial logic rather than parsing edge cases.
Ready to build? Get a free Lekha API key at lekhadev.com and explore your documents as structured JSON in the interactive playground before writing a line of agent code.