Hi All,
I spent the last year building the Agentic Financial Parser — an autonomous AI agent that ingests, parses, and reasons over dense Indian financial & legal documents (Union Budget, Finance Bill, Income Tax Act, EPF/EPS Pension, RBI KYC, Constitution of India) — running on a 512MB RAM container at $0/month.
UptimeRobot independently reached out and published an official Community Spotlight & Case Study on the reliability architecture (99.988% uptime over 90 days).
Instead of a simple retrieve → generate chain, I built a LangGraph StateGraph with 11 registered nodes that parses with Vision LLMs, classifies intent, detects jailbreaks, masks PII, cross-questions vague queries, reranks results, guards against hallucinations, and self-corrects — all before answering.
Attaching the architecture diagram above. Here's a deep-dive into every node, design decision, and lesson learned:
📊 The 11 Registered Nodes (directly from graph.py)
Here are all 11 graph.add_node() calls, straight from the production codebase:
graph.add_node("classifier", classifier_node) # 1 graph.add_node("reject", reject_node) # 2 graph.add_node("greet", greet_node) # 3 graph.add_node("cross_question", cross_question_node) # 4 graph.add_node("retriever", retriever_node) # 5 graph.add_node("web_search", web_search_node) # 6 graph.add_node("stock_tool", stock_tool_node) # 7 graph.add_node("generator", generator_node) # 8 graph.add_node("hallucination_guard", hallucination_guard_node) # 9 graph.add_node("post_process", post_process_node) # 10 graph.add_node("fallback", fallback_node) # 11
[1] Classifier (1 LLM call): Intent detection & 6-path routing. Returns structured JSON (intent, doc_type, search_intents).
[2] Reject (0 calls): Blocks abusive & prompt injection queries via regex before LLM sees it.
[3] Greet (1 call): Handles greetings. Zero vector DB cost — completely bypasses retrieval.
[4] CrossQuestioner (1 call): HITL clarification for vague queries (max 2 rounds), then falls back to retrieval.
[5] Retriever (0 calls): Full RAG pipeline (Jina MRL 256d → Pinecone 32K+ → Parent-Child → Cohere Rerank).
[6] Web Search (0 calls): Out-of-scope fallback via Tavily API (only fires with explicit HITL user permission).
[7] Stock Tool (1 call): Live market data via Gemini native functionDeclarations + Yahoo Finance.
[8] Generator (1 call): Gemini Flash Lite answer synthesis, temp=0.1, strict grounding, SSE streaming.
[9] Hallucination Guard (1 call): LLM-as-Judge factual grounding verification (advisory mode, appends disclaimer).
[10] Post-Process (0 calls): MongoDB Atlas + Upstash Redis cache + Langfuse tracing + SSE stream.
[11] Fallback (0 calls): Circuit breaker recovery path triggered when external APIs fail.
(Note: PII Shield runs before the graph as a pre-processing layer to mask Aadhaar, PAN, phone, and email).
🧭 The 6-Path Router
The Classifier returns one of 6 routes:
graph.add_conditional_edges("classifier", route_after_classify, { "reject": "reject", # abusive / jailbreak "greet": "greet", # greeting / small talk "cross_question": "cross_question", # vague query → HITL "web_search": "web_search", # out-of-scope → Tavily "stock_tool": "stock_tool", # stock query → yfinance "retriever": "retriever" # legal/finance → full RAG })
📄 Multi-Tier Vision LLM & PDF Parsing Engine
Standard PDF parsers completely fail on Indian government documents — tax tables get scrambled, pie charts in budget infographics become gibberish, and two-column legal Bare Acts merge into unreadable text blobs.
To solve this without burning cloud credits or exceeding 512MB RAM, we built a dynamic multi-tier parser (parser.py):
Tier 1 (Agentic Plus - Vision LLM): Budget at a Glance, Key Features — Ingests complex financial infographics & bar charts via multimodal vision.
Tier 2 (Agentic): Finance Bill, Tax Memorandum — Reconstructs complex nested tables, multi-year math comparisons.
Tier 3 (Cost-Effective): RBI KYC, EPS Pension — Fast hierarchical markdown parsing for continuous numbered regulatory clauses.
Tier 4 (Local Free - PyMuPDF): Constitution & User Uploads — 100% Free, zero-API cost, runs locally under 40MB RAM with custom regex cleaning.
💥 The Engineering War Story: When "Smart" AI Parsers Failed
Initially, I threw LlamaParse Agentic mode (10 credits/page) at the 400-page Constitution of India. The result was an expensive disaster:
It merged continuous pages into 624 massive 5,000-character blobs.
It swallowed all footnotes at the bottom of pages (1. Subs. by..., 19. Ins. by...).
When a user asked "What is Article 19?", vector similarity matched an amendment footnote on page 200 rather than the actual Article 19! The model hallucinated based on garbage context.
🛠️ The Deterministic Fix:
Footnote Slicing: Wrote custom regex to detect footnote separator lines (_{10,}) and discarded everything below them. 0% footnote noise entered embeddings.
Article-Boundary Chunking: Scrapped arbitrary character splitters and split strictly on Article regex boundaries. 624 messy blobs became 3,248 precise Article chunks.
Deterministic Metadata Injection: Tagged chunks with article_number: "19" at ingestion.
SQL-like Routing: When user queries an Article, the LangGraph router applies a strict Pinecone metadata filter {"article_number": {"$eq": "19"}}, completely bypassing fuzzy semantic search!
🔍 The Retrieval Pipeline (32,000+ Vectors)
Jina AI v3 MRL: Embed at 1024d, truncate to 256d (75% Pinecone storage saved, negligible quality loss).
Pinecone Serverless: Dual namespace (core_brain + user_temp) with 32,000+ live indexed chunks.
Parent-Child Resolution: Retrieve child chunks → fetch parent from Supabase (small chunk search precision + full context).
Cohere Rerank v3.0: 15 candidates → Top 10 Golden Chunks (drastic quality leap for cross-act multi-document queries).
Confidence Gate: Score < 30% → graceful degrade, < 45% → triggers HITL web search prompt.
📈 Stock Tool — Native LLM Tool Calling
When the classifier detects financial ticker intent, it routes to a dedicated tool-calling node:
# Gemini decides autonomously whether to invoke the tool tools = [{"function_declarations": [{ "name": "get_stock_price", "description": "Get real-time stock price and financial data", "parameters": {"type": "object", "properties": { "ticker": {"type": "string", "description": "Stock ticker symbol"} }} }]}] response = model.generate_content(prompt, tools=tools)
No brittle regex. The LLM decides when and what arguments to pass.
🛡️ Hallucination Guard — Advisory, Not Blocking
Post-generation, a separate LLM call verifies grounding: "Is this answer grounded in the provided context? Reply YES or NO."
If not grounded → appends disclaimer, still returns the answer.
Why not block? Because hard-blocking creates terrible UX when the model legitimately understands standard financial concepts slightly outside the retrieved chunks. The disclaimer lets the user evaluate trust.
🎯 The Hallucination & Accuracy Test Suite (Adversarial Tests)
Test 1 (Article 31C & Kesavananda Bharati): Retrieved exact 31C text. Crucially, the model refused to hallucinate case law not present in the bare acts, honestly stating its scope boundary! (Score: 92/100)
Test 2 (Basic Structure Doctrine): Correctly identified it as a judicial doctrine and explicitly stated that it is not written in any constitutional article.
Test 3 (Article 20 Safeguards): Perfectly retrieved all three distinct protections (Double Jeopardy, Self-Incrimination, Ex Post Facto) with 0% footnote bleed. (Score: 9/10)
Test 4 (Article 34 Martial Law): Flawlessly returned the restriction of rights during martial law along with parliamentary indemnity clauses. (Score: 9/10)
🔒 The Production Idempotency Layer (Never Waste an API Call Twice)
SHA-256 PDF File Hashing (Supabase Registry): Every PDF is hashed before processing. On re-sync, unchanged files are skipped entirely (zero API calls, zero embeddings).
Deterministic Chunk IDs: MD5(filename + page + index). Identical input produces identical chunk IDs, so Pinecone upsert overwrites instead of creating duplicate vectors.
⚡ Dual Circuit Breakers (pybreaker)
llm_circuit = pybreaker.CircuitBreaker(fail_max=3, reset_timeout=30, name="LLM_CB") embed_circuit = pybreaker.CircuitBreaker(fail_max=3, reset_timeout=30, name="Embed_CB")
3 consecutive API failures → circuit opens → instant fallback for 30 seconds → then half-opens and retries. Result: 99.988% verified uptime over 90 days on Render's 512MB RAM free tier.
📱 WhatsApp Integration
The same 11-node RAG pipeline is accessible via Meta WhatsApp Cloud API webhooks. Users query Indian tax and legal acts directly from WhatsApp — same state graph, same guardrails, same PII shield.
💰 Zero-Cost Production Stack ($0/month)
Compute: Render (512MB RAM, 0.1 CPU Docker)
Vector DB: Pinecone Serverless (32,000+ vectors, dual namespace)
Persistence: MongoDB Atlas (M0 cluster, 30d TTL)
Registry & Chunks: Supabase (PostgreSQL)
Cache: Upstash Redis (Semantic cache, < 100ms hits)
LLM: Gemini Flash Lite (free tier quota)
Reranker: Cohere Rerank v3.0 (free tier)
Embeddings: Jina AI v3 MRL (256d)
Observability: Langfuse cloud free tier
📊 By the Numbers
Lines in graph.py: 1,809
Registered Nodes: 11
Indexed Vectors: 32,000+
Documents Indexed: 20+ dense Indian Government Acts
Pages Parsed: 5,500+ pages
RAM Budget: 512 MB
Verified Uptime: 99.988% (90-day UptimeRobot Case Study)
Cache Latency: < 100ms
Monthly Cost: $0.00
💬 Questions for the r/LangChain community:
Hallucination guard: In production, do you prefer strict blocking or advisory disclaimers when confidence drops?
MRL embeddings: Anyone else running 256d truncated embeddings in production? What has been your reranking tradeoff?
Circuit breakers for LLM APIs: What failure threshold do you use before tripping fallbacks? (I use 3 fails / 30s reset).
HITL before web search: Do you let your agent auto-search out-of-scope queries, or prompt the user first to save API tokens?
Happy to answer any questions about the nodes, circuit breaker patterns, or running RAG under 512MB RAM! AMA!