← Back to blog
·10 min read

LlamaIndex + Lekha: Financial Document RAG for Indian Fintech

Build a financial document Q&A agent with LlamaIndex and Lekha. Parse Indian bank statements, ITRs, and CAS reports into structured JSON for RAG pipelines.

llamaindexragai agentbank statement parserindian fintechdocument extractiontypescriptfinancial documents

LlamaIndex is the go-to framework for Retrieval-Augmented Generation (RAG) pipelines and document-aware AI agents. Its core strength — turning unstructured documents into something a language model can reason over — maps perfectly onto the problem of Indian financial documents.

The challenge: Indian financial documents aren't unstructured prose. A bank statement from HDFC looks nothing like one from SBI. A CAS report from CAMS uses different field names than one from Karvy. A salary slip from an MNC payroll system has different sections than one from a small-business accountant. Standard RAG — chunk, embed, retrieve — loses the structure that makes these documents useful.

The solution is to extract structured JSON first with Lekha, then hand that JSON to LlamaIndex. You get the best of both worlds: precise, machine-readable data from the extraction layer, and natural-language reasoning from the LlamaIndex agent layer.

This guide shows you how to wire them together in TypeScript.

What You'll Build

A LlamaIndex agent that:

  • Accepts any Indian financial document (PDF or image)
  • Calls a Lekha tool to extract structured JSON
  • Answers natural-language questions about the document using the extracted data
  • Supports follow-up questions across multiple documents in a session
  • Practical use cases: loan underwriting assistants, portfolio review bots, tax advisory agents, expense audit tools.

    Prerequisites

  • Node.js 18+ or Bun
  • A Lekha API key — get one free at lekhadev.com
  • An Anthropic API key (or swap for any LlamaIndex-supported LLM)
  • Step 1: Install Dependencies

    bun add llamaindex @anthropic-ai/sdk
    

    LlamaIndex's TypeScript SDK (llamaindex) ships with tool-use and agent support out of the box. No separate agent package needed.

    Step 2: Create the Lekha Tool

    LlamaIndex tools are plain TypeScript functions with a schema. Lekha's /extract endpoint accepts a base64-encoded file and returns structured JSON — making it a natural fit.

    // src/tools/lekha-extract.ts
    import { FunctionTool } from "llamaindex";
    

    const LEKHA_API_KEY = process.env.LEKHA_API_KEY!; const LEKHA_BASE_URL = "https://lekhadev.com/api";

    interface ExtractParams { fileBase64: string; mimeType: "application/pdf" | "image/jpeg" | "image/png"; filename: string; }

    async function extractDocument(params: ExtractParams): Promise { const response = await fetch(${LEKHA_BASE_URL}/extract, { method: "POST", headers: { Authorization: Bearer ${LEKHA_API_KEY}, "Content-Type": "application/json", }, body: JSON.stringify({ file: params.fileBase64, mimeType: params.mimeType, filename: params.filename, }), });

    if (!response.ok) { const error = await response.json(); throw new Error(Lekha extraction failed: ${error.error?.message}); }

    const result = await response.json(); // Return the structured data as a formatted JSON string // so the agent can reason over it directly return JSON.stringify(result.data, null, 2); }

    export const lekhaExtractTool = FunctionTool.from(extractDocument, { name: "extract_financial_document", description: Extract structured data from an Indian financial document. Supports: bank statements (HDFC, ICICI, SBI, Axis, Kotak, Yes Bank, IndusInd, and 20+ others), CAS reports (CAMS/Karvy), salary slips, ITR documents, Form 16, CIBIL reports, GST invoices, balance sheets. Returns structured JSON with amounts as numbers and dates in ISO 8601 format., parameters: { type: "object", properties: { fileBase64: { type: "string", description: "Base64-encoded file content", }, mimeType: { type: "string", enum: ["application/pdf", "image/jpeg", "image/png"], description: "MIME type of the file", }, filename: { type: "string", description: "Original filename, used to help classify the document type", }, }, required: ["fileBase64", "mimeType", "filename"], }, });

    Step 3: Build the Agent

    // src/agent.ts
    import { OpenAIAgent, Anthropic } from "llamaindex";
    import { lekhaExtractTool } from "./tools/lekha-extract";
    import fs from "fs";
    import path from "path";
    

    // Helper to encode a local file as base64 function encodeFile(filePath: string): { fileBase64: string; mimeType: string; filename: string; } { const buffer = fs.readFileSync(filePath); const ext = path.extname(filePath).toLowerCase(); const mimeMap: Record = { ".pdf": "application/pdf", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", };

    return { fileBase64: buffer.toString("base64"), mimeType: mimeMap[ext] ?? "application/pdf", filename: path.basename(filePath), }; }

    async function createFinancialAgent() { // Use Claude as the reasoning LLM const llm = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, model: "claude-sonnet-4-6", });

      const agent = new OpenAIAgent({ tools: [lekhaExtractTool], llm, verbose: false, systemPrompt: You are a financial document analyst specializing in Indian finance. When the user provides a document, extract it using the extract_financial_document tool. Then answer questions about the data precisely:
    • Quote exact amounts (numbers, not strings like "Rs. X")
    • Reference specific dates in DD MMM YYYY format
    • For bank statements, calculate summaries across all transactions
    • Flag anomalies or notable patterns proactively,
    • });

    return { agent, encodeFile }; }

    // Main: analyze a bank statement async function main() { const { agent, encodeFile } = await createFinancialAgent();

    const file = encodeFile("./samples/hdfc-statement.pdf");

      // First turn: extract and summarize const summary = await agent.chat({ message: Here is my bank statement. Extract it and give me:
    • Total credits and debits for the period
    • Top 5 spending categories
    • Any large transactions above ₹50,000
    • File: ${JSON.stringify(file)}
      , });

    console.log("Summary:\n", summary.response);

    // Follow-up: the agent retains context from the extraction const followUp = await agent.chat({ message: "What was my average monthly spend on UPI transactions?", });

    console.log("\nFollow-up:\n", followUp.response); }

    main().catch(console.error);

    Run it:

    LEKHA_API_KEY=lk_live_xxx ANTHROPIC_API_KEY=sk-ant-xxx bun run src/agent.ts
    

    Step 4: Multi-Document Analysis

    The real power comes from combining multiple document types. Here's how to build a loan eligibility checker that reads a bank statement and a salary slip together:

    // src/loan-checker.ts
    import { OpenAIAgent, Anthropic, ChatMessage } from "llamaindex";
    import { lekhaExtractTool } from "./tools/lekha-extract";
    import fs from "fs";
    import path from "path";
    

    function toBase64(filePath: string) { const ext = path.extname(filePath).toLowerCase(); return { fileBase64: fs.readFileSync(filePath).toString("base64"), mimeType: ext === ".pdf" ? "application/pdf" : "image/jpeg", filename: path.basename(filePath), }; }

    async function checkLoanEligibility( bankStatementPath: string, salarySlipPath: string, requestedLoanAmount: number, ) { const llm = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, model: "claude-sonnet-4-6", });

      const agent = new OpenAIAgent({ tools: [lekhaExtractTool], llm, systemPrompt: You are a loan underwriting AI for an Indian NBFC. Extract documents with the provided tool, then assess eligibility based on:
    • Debt-to-income ratio (keep below 40%)
    • Income stability (consistent monthly credits)
    • Existing EMI obligations
    • Banking behaviour (minimum balance, overdrafts)
    • Respond with a structured eligibility verdict: APPROVED / REVIEW / REJECTED, with reasoning.
      , });

    const bankFile = toBase64(bankStatementPath); const salaryFile = toBase64(salarySlipPath);

    const result = await agent.chat({ message: Assess loan eligibility for a requested loan of ₹${requestedLoanAmount.toLocaleString("en-IN")}.

      Documents:
    • Bank statement (last 3 months): ${JSON.stringify(bankFile)}
    • Latest salary slip: ${JSON.stringify(salaryFile)}

    Extract both documents, cross-verify the salary, calculate FOIR, and give a final verdict., });

    return result.response; }

    // Example usage checkLoanEligibility( "./samples/bank-statement-3months.pdf", "./samples/salary-slip-may.pdf", 500000, ).then(console.log);

    Step 5: Building a RAG Pipeline with Extracted Data

    For applications where you need to search across many documents (e.g., querying 12 months of statements), store the Lekha-extracted JSON in LlamaIndex's vector store rather than raw PDF text. Structured JSON embeddings are far more precise for financial queries.

    // src/financial-rag.ts
    import {
      VectorStoreIndex,
      Document,
      SimpleDirectoryReader,
      Anthropic,
      storageContextFromDefaults,
    } from "llamaindex";
    

    interface TransactionRecord { documentId: string; month: string; bank: string; data: unknown; }

    async function buildFinancialIndex(extractedRecords: TransactionRecord[]) { // Convert each Lekha extraction result into a LlamaIndex Document const documents = extractedRecords.map( (record) => new Document({ text: JSON.stringify(record.data, null, 2), metadata: { documentId: record.documentId, month: record.month, bank: record.bank, type: "bank_statement", }, }), );

    const storageContext = await storageContextFromDefaults({ persistDir: "./storage/financial-index", });

    const index = await VectorStoreIndex.fromDocuments(documents, { storageContext, });

    return index; }

    async function queryFinancialHistory( index: VectorStoreIndex, question: string, ) { const llm = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, model: "claude-sonnet-4-6", });

    const queryEngine = index.asQueryEngine({ llm }); const response = await queryEngine.query({ query: question }); return response.response; }

    // Usage: answer questions across 12 months of statements // await queryFinancialHistory(index, "How did my grocery spend change from Jan to Jun?"); // await queryFinancialHistory(index, "What is my total investment in SIPs this year?");

    Why Extract First, Then RAG?

    Standard RAG on raw PDF text fails for financial documents because:

    | Approach | Problem | | ----------------------------- | ------------------------------------------------------------------------------- | | Raw PDF text → chunk → embed | "₹1,45,000" vs "1.45 lakh" vs "145000" → same amount, three embeddings | | OCR text → chunk → embed | Table rows split across chunks, headers lost | | Lekha JSON → Document → embed | All amounts normalized to numbers, dates in ISO 8601, fields named consistently |

    When you embed { "amount": 145000, "date": "2026-03-15", "category": "salary" }, the retrieval is deterministic. The model doesn't need to parse formatting variations — it reasons over clean data.

    Lekha handles the normalization across all 28+ Indian bank formats so your RAG pipeline sees a consistent schema regardless of whether the statement came from SBI, HDFC, or a regional co-operative bank.

    Supported Document Types

    You can pass any of these to extract_financial_document:

  • Bank statements: HDFC, ICICI, SBI, Axis, Kotak, Yes Bank, IndusInd, Federal Bank, PNB, and 20+ more
  • Investment documents: CAS reports (CAMS, Karvy), mutual fund statements
  • Tax documents: ITR, Form 16, Form 26AS
  • Income documents: Salary slips (all payroll formats)
  • Credit documents: CIBIL reports, credit bureau PDFs
  • Business documents: GST invoices, balance sheets, P&L statements
  • Explore all supported formats at lekhadev.com/docs.

    Deployment Notes

    A few things to keep in mind when moving to production:

    File size: Lekha accepts files up to 10 MB. For multi-page statements, send the entire PDF — the extraction handles pagination internally. Caching: Extraction results are deterministic for the same document. Cache by file hash to avoid redundant API calls:
    import crypto from "crypto";
    

    function hashFile(buffer: Buffer): string { return crypto.createHash("sha256").update(buffer).digest("hex"); }

    const cache = new Map();

    async function extractWithCache(filePath: string) { const buffer = fs.readFileSync(filePath); const hash = hashFile(buffer);

    if (cache.has(hash)) return cache.get(hash);

    const result = await extractDocument({ fileBase64: buffer.toString("base64"), mimeType: "application/pdf", filename: path.basename(filePath), });

    cache.set(hash, result); return result; }

    Rate limits: The free tier supports 100 extractions/month. Paid plans start from 1,000 extractions/month — see lekhadev.com for current pricing.

    FAQ

    Can I use LlamaIndex's built-in PDF reader instead of Lekha?

    You can, but LlamaIndex's SimpleDirectoryReader uses text extraction (pdfjs) which loses table structure. For Indian financial documents — which are almost entirely tables — Lekha's vision-based extraction is significantly more accurate, especially for scanned PDFs and statements with merged cells.

    Does this work with Python LlamaIndex?

    Yes. The Lekha API is HTTP-based, so the pattern is identical in Python. Use requests or httpx to call /extract, then wrap the result in a LlamaIndex FunctionTool. The LangChain guide shows the Python equivalent.

    What model should I use for the agent?

    Claude Sonnet 4.6 works well for financial reasoning — it handles Indian terminology (FOIR, HRA, TDS, GSTIN) reliably. For cost-sensitive pipelines, Haiku 4.5 handles simpler summarization tasks at lower cost.

    How do I handle confidential documents?

    Lekha processes documents in memory and does not store them after extraction. See the DPDP compliance guide for a full breakdown of the data handling guarantees.

    What's Next

    You now have a LlamaIndex agent that understands Indian financial documents. From here:

  • Add persistent memory with LlamaIndex's ChatMemoryBuffer so users can ask follow-ups across sessions
  • Connect the index to a Supabase vector store for multi-user deployments
  • Build a Slack or WhatsApp interface on top of the agent loop
  • Test the extraction live at lekhadev.com/playground — upload any Indian financial PDF and see the JSON output before writing a single line of code.

    Ready to build? Sign up at lekhadev.com and get your API key in under a minute.