← Back to blog
·10 min read

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.

agnophidatabank statement parsingai agentpythonfintech indiadocument extractionfinancial ai
Agno (formerly phidata) is a Python-native agent framework built for speed and simplicity. Agents initialize in microseconds, tool definitions are plain Python functions, and multi-agent "teams" coordinate through a shared session. When you pair Agno with Lekha, you get a production-ready stack for processing Indian financial documents — bank statements, salary slips, ITR, CAS reports, and more — without writing parsing logic from scratch.

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:
  • Agent teams: Route tasks to specialist agents (extraction, analysis, compliance checks) with a team coordinator
  • Built-in storage: Persist agent sessions to SQLite or PostgreSQL — agent context survives request boundaries
  • Structured outputs: Agents return typed Pydantic models, not raw strings
  • Any LLM backend: Claude, OpenAI, Groq, Ollama, Gemini — swap with one line
  • 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.

    Does Agno work with open-source models like Llama 3 or Qwen?

    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.

    How does Agno agent memory differ from LangChain memory?

    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.