<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Nitya]]></title><description><![CDATA[Nitya]]></description><link>https://nityamalhotra.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 16 Sep 2026 09:50:18 GMT</lastBuildDate><atom:link href="https://nityamalhotra.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Penny-Pinching Detective: How My AI Tracks Every Token and Cent 🕵️‍♂️💰]]></title><description><![CDATA[Chapter 1: The Tokenization Detective Story 🔍
Picture this: A user types "What are the requirements for filing a bail application under CrPC?" - seems like a simple question, right? But behind the scenes, my AI immediately springs into action like a...]]></description><link>https://nityamalhotra.hashnode.dev/the-penny-pinching-detective-how-my-ai-tracks-every-token-and-cent</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/the-penny-pinching-detective-how-my-ai-tracks-every-token-and-cent</guid><category><![CDATA[generative ai]]></category><category><![CDATA[Tokenization]]></category><category><![CDATA[genai]]></category><category><![CDATA[AI]]></category><category><![CDATA[openai]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Fri, 03 Oct 2025 12:24:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/jpqyfK7GB4w/upload/8011827917b09428123a4200a645f9e0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-chapter-1-the-tokenization-detective-story"><strong>Chapter 1: The Tokenization Detective Story 🔍</strong></h2>
<p>Picture this: A user types <em>"What are the requirements for filing a bail application under CrPC?"</em> - seems like a simple question, right? But behind the scenes, my AI immediately springs into action like a forensic detective examining evidence.</p>
<p>The first suspect to interrogate? <strong>The text itself</strong>. My TokenCalculator doesn’t just guess how many tokens this question contains - it performs a precise autopsy using tiktoken, OpenAI's own dissection tool.</p>
<p>Here's where it gets fascinating: The system first determines which "scalpel" to use. Different AI models slice text differently, like different languages having different alphabets:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_get_encoding_for_model</span>(<span class="hljs-params">self, model: str</span>) -&gt; str:</span>
    <span class="hljs-keyword">if</span> model.startswith((<span class="hljs-string">"gpt-4"</span>, <span class="hljs-string">"gpt-3.5-turbo"</span>)):
        <span class="hljs-keyword">return</span> <span class="hljs-string">"cl100k_base"</span>
    <span class="hljs-keyword">elif</span> model.startswith(<span class="hljs-string">"text-davinci"</span>):
        <span class="hljs-keyword">return</span> <span class="hljs-string">"p50k_base"</span>
</code></pre>
<p>Why does this matter? Because when GPT-3.5-turbo sees the word "requirements", it might break it into 2 tokens: ["require", "ments"]. But an older model might see it as 3 tokens: ["req", "uire", "ments"]. Using the wrong dissection tool would be like using a metric ruler to measure inches - completely wrong results!</p>
<h2 id="heading-chapter-2-the-caching-genius-performance-under-pressure"><strong>Chapter 2: The Caching Genius - Performance Under Pressure ⚡</strong></h2>
<p>Here's where my system shows its intelligence: creating these tokenization tools is expensive (like setting up a crime lab), but once you have them, using them is fast. So my system caches them:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Get or create cached encoder</span>
<span class="hljs-keyword">if</span> encoding_name <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> self._encoders:
    self._encoders[encoding_name] = tiktoken.get_encoding(encoding_name)
    logger.debug(<span class="hljs-string">f"Created tokenizer for encoding: <span class="hljs-subst">{encoding_name}</span>"</span>)

encoder = self._encoders[encoding_name]
tokens = encoder.encode(text)
token_count = len(tokens)
</code></pre>
<p>When the user's question arrives, the system checks: "Do I already have the cl100k_base tokenizer ready?" If yes, it immediately dissects the text. If no, it creates the tokenizer once and saves it for future use.</p>
<p>The result? That bail application question gets precisely dissected: <em>"What are the requirements for filing a bail application under CrPC?"</em> becomes exactly <strong>15 tokens</strong>. Not 14, not 16 - exactly 15.</p>
<h2 id="heading-chapter-3-the-response-length-prophet-intelligent-prediction"><strong>Chapter 3: The Response Length Prophet - Intelligent Prediction 🔮</strong></h2>
<p>Now comes the most sophisticated part: My system needs to predict how long the AI's response will be <strong>before</strong> actually calling it. This isn't just a wild guess - it's a multi-layered analysis that would make a detective proud.</p>
<p>The _estimate_response_length method performs a complete psychological profile of the question:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_estimate_response_length</span>(<span class="hljs-params">self, user_question: str, model: str</span>) -&gt; int:</span>
    <span class="hljs-comment"># Step 1: Count input tokens</span>
    input_tokens = count_tokens(user_question, model)

    <span class="hljs-comment"># Step 2: How complex is the question?</span>
    complexity_factor = self._analyze_question_complexity(user_question, input_tokens)

    <span class="hljs-comment"># Step 3: Model-specific response patterns</span>
    model_response_factor = self._get_model_response_factor(model)

    <span class="hljs-comment"># Step 4: Calculate estimated response length</span>
    base_response_tokens = int(input_tokens * complexity_factor * model_response_factor)
</code></pre>
<h3 id="heading-the-complexity-detective-work">The Complexity Detective Work</h3>
<p>The system analyzes the question like a forensic psychologist, looking for clues about how complex the answer needs to be:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_analyze_question_complexity</span>(<span class="hljs-params">self, question: str, input_tokens: int</span>) -&gt; float:</span>
    complexity_score = <span class="hljs-number">1.0</span>  <span class="hljs-comment"># Base complexity</span>

    <span class="hljs-comment"># Factor 1: Length of question</span>
    <span class="hljs-keyword">if</span> input_tokens &gt; <span class="hljs-number">500</span>:
        complexity_score += <span class="hljs-number">0.5</span>  <span class="hljs-comment"># Long question = detailed answer</span>
    <span class="hljs-keyword">elif</span> input_tokens &gt; <span class="hljs-number">200</span>:
        complexity_score += <span class="hljs-number">0.2</span>  <span class="hljs-comment"># Medium length</span>

    <span class="hljs-comment"># Factor 2: Question words that indicate complexity</span>
    complexity_indicators = {
        <span class="hljs-string">'analyze'</span>: <span class="hljs-number">0.6</span>,       <span class="hljs-comment"># Analysis requests need detailed responses</span>
        <span class="hljs-string">'comprehensive'</span>: <span class="hljs-number">0.7</span>,  <span class="hljs-comment"># User wants thorough explanation</span>
        <span class="hljs-string">'explain'</span>: <span class="hljs-number">0.3</span>,       <span class="hljs-comment"># Needs explanation</span>
        <span class="hljs-string">'compare'</span>: <span class="hljs-number">0.4</span>,       <span class="hljs-comment"># Comparison = longer answer</span>
        <span class="hljs-string">'contract'</span>: <span class="hljs-number">0.4</span>,      <span class="hljs-comment"># Legal complexity</span>
        <span class="hljs-string">'liability'</span>: <span class="hljs-number">0.4</span>,     <span class="hljs-comment"># Complex legal concepts</span>
        <span class="hljs-string">'briefly'</span>: <span class="hljs-number">-0.3</span>,      <span class="hljs-comment"># User wants short answer</span>
        <span class="hljs-string">'simple'</span>: <span class="hljs-number">-0.2</span>,       <span class="hljs-comment"># Keep it simple</span>
        <span class="hljs-string">'yes/no'</span>: <span class="hljs-number">-0.4</span>        <span class="hljs-comment"># Binary answer expected</span>
    }
</code></pre>
<p>For our bail application question, the system finds:</p>
<ul>
<li><p>"requirements" → indicates detailed explanation needed (+0.3)</p>
</li>
<li><p>"filing" → legal procedure complexity (+0.3)</p>
</li>
<li><p>"application" → formal process needs thorough explanation (+0.2)</p>
</li>
<li><p>"CrPC" → specific legal reference needs detailed answer (+0.4)</p>
</li>
</ul>
<p><strong>Final complexity score: 1.0 + 0.3 + 0.3 + 0.2 + 0.4 = 2.2</strong></p>
<h3 id="heading-the-model-personality-profile">The Model Personality Profile</h3>
<p>Different AI models have different "personalities" when responding:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_get_model_response_factor</span>(<span class="hljs-params">self, model: str</span>) -&gt; float:</span>
    model_factors = {
        <span class="hljs-string">'gpt-4'</span>: <span class="hljs-number">1.3</span>,          <span class="hljs-comment"># More thorough and detailed</span>
        <span class="hljs-string">'gpt-4-32k'</span>: <span class="hljs-number">1.4</span>,      <span class="hljs-comment"># Even more comprehensive</span>
        <span class="hljs-string">'gpt-3.5-turbo'</span>: <span class="hljs-number">1.0</span>,  <span class="hljs-comment"># More concise</span>
        <span class="hljs-string">'gpt-3.5-turbo-16k'</span>: <span class="hljs-number">1.1</span>,
    }
</code></pre>
<h3 id="heading-the-final-calculation">The Final Calculation</h3>
<p>For our 15-token bail question with GPT-3.5-turbo:</p>
<ul>
<li><p>Input tokens: 15</p>
</li>
<li><p>Complexity factor: 2.2 (detailed legal explanation needed)</p>
</li>
<li><p>Model factor: 1.0 (GPT-3.5-turbo is concise)</p>
</li>
<li><p><strong>Base estimate: 15 × 2.2 × 1.0 = 33 tokens</strong></p>
</li>
</ul>
<p>But wait! The system applies safety bounds:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Step 5: Apply reasonable bounds</span>
min_response = max(<span class="hljs-number">50</span>, input_tokens * <span class="hljs-number">0.2</span>)  <span class="hljs-comment"># At least 50 tokens or 20% of input</span>
max_response = min(<span class="hljs-number">2000</span>, input_tokens * <span class="hljs-number">3.0</span>)  <span class="hljs-comment"># At most 2000 tokens or 3x input</span>

estimated_tokens = max(min_response, min(max_response, base_response_tokens))
</code></pre>
<p>Since 33 is less than the minimum of 50, the final estimate becomes <strong>50 tokens</strong> for a simple question, or up to <strong>500+ tokens</strong> for more complex legal questions.</p>
<h2 id="heading-chapter-4-the-budget-security-guard-the-ultimate-gatekeeper"><strong>Chapter 4: The Budget Security Guard - The Ultimate Gatekeeper 🛡️</strong></h2>
<p>But here's where your system shows its true genius: <strong>It doesn't just track costs after the fact - it prevents expensive requests from even happening!</strong></p>
<p>Enter the CostMonitoringMiddleware - a vigilant security guard that stands at the entrance of your AI system, checking every single request like a bouncer at an exclusive club.</p>
<h3 id="heading-the-security-checkpoint-process">The Security Checkpoint Process</h3>
<p>Every time someone asks a legal question, this security guard springs into action with a multi-step verification process:</p>
<ol>
<li><p><strong>Does this request need budget checking?</strong> - Smart filtering between AI endpoints (costly) and free endpoints (health checks, auth, etc.)</p>
</li>
<li><p><strong>Who is making this request?</strong> - Extracts user ID from authentication tokens</p>
</li>
<li><p><strong>How much will this cost?</strong> - Uses the same intelligent estimation we discussed earlier</p>
</li>
<li><p><strong>Can this user afford it?</strong> - Checks against their budget limits</p>
</li>
</ol>
<h3 id="heading-the-budget-enforcement-decision">The Budget Enforcement Decision</h3>
<p>Here's the critical moment: If a user has exceeded their daily budget of $5.00 and this request would cost an estimated $0.0007165, the system makes an instant decision:</p>
<p><strong>BLOCK THE REQUEST</strong> ❌</p>
<p>The response is immediate - a 429 "Too Many Requests" status with a clear message:</p>
<ul>
<li><p>"Budget exceeded - Daily limit reached"</p>
</li>
<li><p>"Estimated cost: $0.0007165"</p>
</li>
<li><p>"Suggestion: Upgrade your plan"</p>
</li>
</ul>
<p><strong>The AI is never called. The user is never charged. The budget is protected.</strong></p>
<h2 id="heading-chapter-5-the-real-time-surveillance-operation"><strong>Chapter 5: The Real-Time Surveillance Operation 🎭</strong></h2>
<p>Now that we've seen how the security guard protects the entrance, let's witness what happens when a request is approved and makes it through to the AI. This is where the second layer of our financial intelligence system kicks in - the real-time surveillance operation.</p>
<p>The CostTrackingCallback is like a covert operative that has been waiting patiently in the shadows. The moment a request passes the budget checkpoint, this callback quietly attaches itself to the conversation like a wiretap:</p>
<pre><code class="lang-python">cost_callback = CostTrackingCallback(user_id=payload.user_id, request_id=request_id)
llm = ChatOpenAI(callbacks=[cost_callback])  <span class="hljs-comment"># Surveillance activated!</span>
</code></pre>
<p>The callback then waits patiently. When the AI finishes generating its response, the callback immediately pounces on the raw data like a detective examining fresh evidence:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_llm_end</span>(<span class="hljs-params">self, response: Any, **kwargs: Any</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    <span class="hljs-comment"># Extract the actual token usage from OpenAI's response</span>
    usage = llm_output.get(<span class="hljs-string">'token_usage'</span>, {})
    input_tokens = int(usage.get(<span class="hljs-string">"prompt_tokens"</span>, <span class="hljs-number">0</span>))
    output_tokens = int(usage.get(<span class="hljs-string">"completion_tokens"</span>, <span class="hljs-number">0</span>))
</code></pre>
<p>Here's what actually happens: OpenAI sends back a detailed forensic report hidden in the response metadata:</p>
<pre><code class="lang-python">{
  <span class="hljs-string">"token_usage"</span>: {
    <span class="hljs-string">"prompt_tokens"</span>: <span class="hljs-number">15</span>,
    <span class="hljs-string">"completion_tokens"</span>: <span class="hljs-number">347</span>,  // Not the <span class="hljs-number">50</span><span class="hljs-number">-500</span> estimate!
    <span class="hljs-string">"total_tokens"</span>: <span class="hljs-number">362</span>
  }
}
</code></pre>
<p>My callback detective immediately seizes this evidence: "Wait! The prediction was 50-500 tokens, but the actual response was 347 tokens!" The system then calculates the <strong>exact</strong> cost using real numbers.</p>
<h2 id="heading-the-detectives-masterpiece-complete-financial-intelligence"><strong>The Detective's Masterpiece: Complete Financial Intelligence 🎯</strong></h2>
<p>When someone asks <em>"What are the requirements for filing a bail application under CrPC?"</em>, here's the <strong>complete</strong> financial intelligence process:</p>
<h3 id="heading-pre-request-middleware-guardian">Pre-Request (Middleware Guardian)</h3>
<ol>
<li><p><strong>Request Interception</strong>: Middleware catches the request</p>
</li>
<li><p><strong>User Identification</strong>: Extracts user ID from token</p>
</li>
<li><p><strong>Cost Estimation</strong>: Analyzes question complexity → estimates 347 tokens response</p>
</li>
<li><p><strong>Budget Check</strong>: Verifies user can afford estimated $0.0007165</p>
</li>
<li><p><strong>Authorization Decision</strong>: ALLOW (if within budget) or BLOCK (if exceeded)</p>
</li>
</ol>
<h3 id="heading-during-request-if-approved">During Request (If Approved)</h3>
<h3 id="heading-ai-processing-question-sent-to-openai-with-callback-attached"><strong>AI Processing</strong>: Question sent to OpenAI with callback attached</h3>
<ol start="6">
<li><strong>Response Generation</strong>: AI generates actual answer (347 tokens)</li>
</ol>
<h3 id="heading-post-request-callback-accountant">Post-Request (Callback Accountant)</h3>
<ol start="8">
<li><p><strong>Real Usage Extraction</strong>: Callback extracts actual token usage from response</p>
</li>
<li><p><strong>Precise Billing</strong>: User charged exactly $0.0007165 for real usage</p>
</li>
<li><p><strong>Audit Recording</strong>: Complete record with estimates vs actuals</p>
</li>
<li><p><strong>Response Headers</strong>: Cost transparency added to user response</p>
</li>
</ol>
<p>The result? A legal AI system with <strong>complete financial intelligence</strong> - predictive cost control, real-time budget enforcement, precise billing, and forensic audit trails. No surprises, no overages, no budget disasters!  </p>
<hr />
<p><em>Our penny-pinching detective has revealed the complete financial intelligence system - from predictive gatekeeping to precise accounting, with intelligent estimation and real-time surveillance working in perfect harmony!</em></p>
<p><em>Next up: The Cache Master - how my AI uses multi-level caching strategies to make expensive operations lightning-fast and cost-effective! ⚡</em></p>
]]></content:encoded></item><item><title><![CDATA[The Answer Architect: How My AI Crafts the Final Legal Response 🏗️]]></title><description><![CDATA[Chapter 1: The Grand Construction Project Begins 🚧
After all the retrieval, reranking, confidence calculations, and backup tool magic, my AI stands before its greatest challenge: building the perfect legal response. This isn't just about generating ...]]></description><link>https://nityamalhotra.hashnode.dev/the-answer-architect-how-my-ai-crafts-the-final-legal-response</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/the-answer-architect-how-my-ai-crafts-the-final-legal-response</guid><category><![CDATA[generative ai]]></category><category><![CDATA[genai]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[openai]]></category><category><![CDATA[#DeveloperJourney]]></category><category><![CDATA[#PromptEngineering]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Fri, 03 Oct 2025 10:09:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/VLPUm5wP5Z0/upload/ebdf55723c0777154f4d37ad564d2644.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-chapter-1-the-grand-construction-project-begins"><strong>Chapter 1: The Grand Construction Project Begins 🚧</strong></h2>
<p>After all the retrieval, reranking, confidence calculations, and backup tool magic, my AI stands before its greatest challenge: building the perfect legal response. This isn't just about generating text—it's about orchestrating a complete, well-sourced, and transparent answer.</p>
<h2 id="heading-chapter-2-preparing-the-foundation-context-engineering"><strong>Chapter 2: Preparing the Foundation - Context Engineering 📐</strong></h2>
<p>Before any construction can begin, my AI needs to lay a solid foundation. Enter the <code>format_context</code> function, the unsung hero of response quality:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">format_context</span>(<span class="hljs-params">retrieved_docs, tool_results</span>):</span>
    <span class="hljs-string">"""Transform messy documents into organized, numbered context"""</span>
    context_parts = []

    <span class="hljs-comment"># Add retrieved documents with metadata</span>
    <span class="hljs-keyword">if</span> retrieved_docs:
        context_parts.append(<span class="hljs-string">"## Retrieved Legal Documents:"</span>)
        <span class="hljs-keyword">for</span> i, doc <span class="hljs-keyword">in</span> enumerate(retrieved_docs, <span class="hljs-number">1</span>):
            title = doc.metadata.get(<span class="hljs-string">'title'</span>, <span class="hljs-string">'Unknown Document'</span>)
            page = doc.metadata.get(<span class="hljs-string">'page'</span>, <span class="hljs-string">'Unknown Page'</span>)
            source = doc.metadata.get(<span class="hljs-string">'source'</span>, <span class="hljs-string">'Unknown Source'</span>)

            context_parts.append(<span class="hljs-string">f"\n### Document <span class="hljs-subst">{i}</span>: <span class="hljs-subst">{title}</span>"</span>)
            context_parts.append(<span class="hljs-string">f"**Source:** <span class="hljs-subst">{source}</span> (Page <span class="hljs-subst">{page}</span>)"</span>)
            context_parts.append(<span class="hljs-string">f"**Content:** <span class="hljs-subst">{doc.page_content}</span>"</span>)

    <span class="hljs-keyword">return</span> <span class="hljs-string">"\n"</span>.join(context_parts)
</code></pre>
<p>This function is like a master organizer—it takes the messy pile of retrieved documents and transforms them into a neat, numbered catalog. Each document gets its own introduction with source, page, and document type, making it easy for the LLM to reference specific information.</p>
<h2 id="heading-chapter-3-the-llm-generation-process"><strong>Chapter 3: The LLM Generation Process 🏗️</strong></h2>
<p>With the foundation ready, my system carefully crafts the perfect prompt with all the dynamic pieces and sends it to the LLM. The LLM becomes the master craftsman, taking the organized context, following detailed instructions, and building a response that's both accurate and beautifully formatted.</p>
<h2 id="heading-chapter-4-the-citation-workshop-building-credibility"><strong>Chapter 4: The Citation Workshop - Building Credibility 📚</strong></h2>
<p>No legal response is complete without proper citations. Enter the <code>extract_citations</code> function, the system's meticulous librarian:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">extract_citations</span>(<span class="hljs-params">response_text, retrieved_docs</span>):</span>
    <span class="hljs-string">"""Extract and format proper legal citations"""</span>
    citations = []
    seen = set()  <span class="hljs-comment"># Avoid duplicates</span>

    <span class="hljs-keyword">for</span> i, doc <span class="hljs-keyword">in</span> enumerate(retrieved_docs, <span class="hljs-number">1</span>):
        doc_reference = <span class="hljs-string">f"Document <span class="hljs-subst">{i}</span>"</span>
        <span class="hljs-keyword">if</span> doc_reference <span class="hljs-keyword">in</span> response_text:
            source_key = <span class="hljs-string">f"<span class="hljs-subst">{doc.metadata.get(<span class="hljs-string">'source'</span>, <span class="hljs-string">'Unknown'</span>)}</span>-<span class="hljs-subst">{doc.metadata.get(<span class="hljs-string">'page'</span>, <span class="hljs-string">'Unknown'</span>)}</span>"</span>

            <span class="hljs-keyword">if</span> source_key <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> seen:
                citation = {
                    <span class="hljs-string">"document_number"</span>: i,
                    <span class="hljs-string">"title"</span>: doc.metadata.get(<span class="hljs-string">'title'</span>, <span class="hljs-string">'Unknown Document'</span>),
                    <span class="hljs-string">"source"</span>: doc.metadata.get(<span class="hljs-string">'source'</span>, <span class="hljs-string">'Unknown Source'</span>),
                    <span class="hljs-string">"page"</span>: doc.metadata.get(<span class="hljs-string">'page'</span>, <span class="hljs-string">'Unknown Page'</span>),
                    <span class="hljs-string">"act"</span>: doc.metadata.get(<span class="hljs-string">'act'</span>, <span class="hljs-string">'Unknown Act'</span>),
                    <span class="hljs-string">"section"</span>: doc.metadata.get(<span class="hljs-string">'section'</span>, <span class="hljs-string">'Unknown Section'</span>)
                }
                citations.append(citation)
                seen.add(source_key)

    <span class="hljs-keyword">return</span> citations
</code></pre>
<h2 id="heading-chapter-5-the-final-assembly-line"><strong>Chapter 5: The Final Assembly Line 🏭</strong></h2>
<p>Now comes the grand finale—assembling everything into the final response structure:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatResponse</span>(<span class="hljs-params">BaseModel</span>):</span>
    response: str                   <span class="hljs-comment"># The main AI answer</span>
    citations: List[Dict]           <span class="hljs-comment"># Extracted citations</span>
    confidence_score: float         <span class="hljs-comment"># How confident we are</span>
    tools_used: Optional[List[str]] <span class="hljs-comment"># Which backup tools helped</span>
    query_processed: str            <span class="hljs-comment"># The cleaned query</span>
    reading_level: str              <span class="hljs-comment"># Response complexity</span>
    response_time: float            <span class="hljs-comment"># How long it took</span>
</code></pre>
<p>No black boxes here! Everything is open, traceable, and auditable.</p>
<h2 id="heading-chapter-6-the-quality-assurance-department"><strong>Chapter 6: The Quality Assurance Department 📊</strong></h2>
<p>Before shipping the response, my system does some final quality checks and housekeeping. The system records performance metrics, tracks detailed metadata about how the response was generated, and ensures everything is properly cached for future requests. (<em>More on this in the upcoming articles</em>)</p>
<h2 id="heading-chapter-7-the-grand-reveal"><strong>Chapter 7: The Grand Reveal 🎭</strong></h2>
<p>Finally, my AI delivers its masterpiece - a carefully crafted legal document that includes:</p>
<ul>
<li><p>🎨 <strong>A properly formatted HTML answer</strong></p>
</li>
<li><p>🎯 <strong>Transparent confidence scoring</strong></p>
</li>
<li><p>📖 <strong>Full citation lists</strong></p>
</li>
<li><p>📊 <strong>Performance metrics</strong></p>
</li>
<li><p>🔧 <strong>Tool usage reports</strong></p>
</li>
</ul>
<p>Every response is a work of art, grounded in data, and built for transparency.</p>
<h2 id="heading-the-architects-philosophy"><strong>The Architect's Philosophy 🏛️</strong></h2>
<p>What makes this answer architecture special isn't just the code—it's the philosophy behind it:</p>
<ul>
<li><p><strong>🔍 Transparency First</strong>: Every piece of information used is documented and shared</p>
</li>
<li><p><strong>⚡ Performance Matters</strong>: Context optimization and caching keep things fast</p>
</li>
<li><p><strong>✨ Quality Over Quantity</strong>: Source deduplication ensures only the best references</p>
</li>
<li><p><strong>👥 User Experience</strong>: HTML formatting and snippets make responses readable</p>
</li>
<li><p><strong>📈 Continuous Improvement</strong>: Detailed metrics help optimize the system</p>
</li>
</ul>
<p>My AI doesn't just answer questions—it builds complete, trustworthy, and transparent legal responses that users can rely on.</p>
<hr />
<p><em>And there we have it! The complete journey from query to response is now complete. Our legal AI has successfully navigated through hybrid retrieval, intelligent reranking, confidence-based tool selection, parallel execution, and final answer architecture.</em></p>
<p><em>Next up in our series: We'll dive deeper into the architectural decisions that make this system robust—from token calculation and cost optimization, to evaluation metrics and performance monitoring. The foundation is built; now let's explore what makes it enterprise-ready! 🏗️</em></p>
]]></content:encoded></item><item><title><![CDATA[Calling for Backup: How AI Tools Step In When Confidence Is Low 🛠️]]></title><description><![CDATA[Chapter 1: The AI’s SOS Moment 🚨
After playing the confidence game, my legal AI sometimes finds itself in a pickle. If confidence is high, it answers boldly. But when confidence drops below 0.3, it gets a little anxious. Instead of guessing, it call...]]></description><link>https://nityamalhotra.hashnode.dev/calling-for-backup-how-ai-tools-step-in-when-confidence-is-low</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/calling-for-backup-how-ai-tools-step-in-when-confidence-is-low</guid><category><![CDATA[generative ai]]></category><category><![CDATA[genai]]></category><category><![CDATA[AI]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Fri, 03 Oct 2025 08:32:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/GoXNygZlftg/upload/451dfc4299d5ab4129f3fade26ecda14.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-chapter-1-the-ais-sos-moment"><strong>Chapter 1: The AI’s SOS Moment 🚨</strong></h2>
<p>After playing the confidence game, my legal AI sometimes finds itself in a pickle. If confidence is high, it answers boldly. But when confidence drops below 0.3, it gets a little anxious. Instead of guessing, it calls for backup—summoning a team of specialized tools to help craft a better answer.</p>
<h2 id="heading-chapter-2-meet-the-backup-crew"><strong>Chapter 2: Meet the Backup Crew 🧑‍🔧</strong></h2>
<p>Let’s peek behind the curtain and see who’s on call in my code. When the AI is unsure, it analyzes the query and picks the right helpers:</p>
<pre><code class="lang-python">tools_to_run = []

<span class="hljs-comment"># Citation tool</span>
<span class="hljs-keyword">if</span> any(keyword <span class="hljs-keyword">in</span> query.lower() <span class="hljs-keyword">for</span> keyword <span class="hljs-keyword">in</span> [<span class="hljs-string">'section'</span>, <span class="hljs-string">'act'</span>, <span class="hljs-string">'law'</span>, <span class="hljs-string">'provision'</span>, <span class="hljs-string">'cite'</span>]):
    tools_to_run.append({
        <span class="hljs-string">'name'</span>: <span class="hljs-string">'fetch_legal_citations'</span>,
        <span class="hljs-string">'func'</span>: fetch_legal_citations,
        <span class="hljs-string">'kwargs'</span>: {<span class="hljs-string">'legal_query'</span>: query, <span class="hljs-string">'jurisdiction'</span>: <span class="hljs-string">'India'</span>}
    })

<span class="hljs-comment"># Section explanation tool</span>
section_pattern = <span class="hljs-string">r'(section|sec|article|art)\s+(\d+[a-z]*)'</span>
<span class="hljs-keyword">if</span> re.search(section_pattern, query.lower()) <span class="hljs-keyword">or</span> complexity_level <span class="hljs-keyword">in</span> [<span class="hljs-string">'simple'</span>, <span class="hljs-string">'beginner'</span>]:
    match = re.search(section_pattern, query.lower())
    section_ref = match.group(<span class="hljs-number">2</span>) <span class="hljs-keyword">if</span> match <span class="hljs-keyword">else</span> query.split()[:<span class="hljs-number">3</span>]
    tools_to_run.append({
        <span class="hljs-string">'name'</span>: <span class="hljs-string">'summarize_legal_section'</span>, 
        <span class="hljs-string">'func'</span>: summarize_legal_section,
        <span class="hljs-string">'kwargs'</span>: {<span class="hljs-string">'section_reference'</span>: str(section_ref), <span class="hljs-string">'complexity_level'</span>: complexity_level}
    })

<span class="hljs-comment"># Case precedent tool (very low confidence only)</span>
<span class="hljs-keyword">if</span> confidence &lt; <span class="hljs-number">0.1</span> <span class="hljs-keyword">and</span> any(keyword <span class="hljs-keyword">in</span> query.lower() <span class="hljs-keyword">for</span> keyword <span class="hljs-keyword">in</span> [<span class="hljs-string">'case'</span>, <span class="hljs-string">'precedent'</span>, <span class="hljs-string">'judgment'</span>, <span class="hljs-string">'ruling'</span>]):
    tools_to_run.append({
        <span class="hljs-string">'name'</span>: <span class="hljs-string">'find_similar_cases'</span>,
        <span class="hljs-string">'func'</span>: find_similar_cases, 
        <span class="hljs-string">'kwargs'</span>: {<span class="hljs-string">'case_facts'</span>: query, <span class="hljs-string">'case_type'</span>: <span class="hljs-string">'any'</span>}
    })
</code></pre>
<h2 id="heading-chapter-3-why-i-built-my-own-backup-system-and-why-two-tools-are-better-than-three"><strong>Chapter 3: Why I Built My Own Backup System (And Why Two Tools Are Better Than Three!) 🎩✨</strong></h2>
<p>You might ask — why not just let LangChain’s <code>AgentExecutor</code> handle tool orchestration?<br />I tried. But it plays it <em>safe</em> — running tools one at a time, like a librarian checking out books solo. My latency graphs? 🐌 Slow-motion replay.</p>
<p>So I built my own <code>execute_tools_parallel</code>. Now my AI fires tools <strong>side-by-side</strong> — each gets a strict 12-second timeout. If one’s too slow or errors out, it’s benched, logged, and we move on. No mystery “agent logic,” just clean, predictable code.</p>
<p>But here’s the kicker: I have three tools… yet two at a time wins.<br />Three caused chaos — resource contention, random slowdowns, everyone shouting over each other. Two feels <em>just right</em> — nimble, fast, controlled.</p>
<p>Result? ⚡ Lightning-quick answers, no hangs, graceful fallbacks. Even if one backup dancer misses a beat, the show goes on.</p>
<h2 id="heading-chapter-4-the-backup-plan-in-action"><strong>Chapter 4: The Backup Plan in Action 🏃</strong></h2>
<p>When the AI’s confidence is low, it doesn’t just pick one tool—it can run several in parallel, but never more than two at once. Here’s how the magic happens:</p>
<pre><code class="lang-python">tool_results = <span class="hljs-keyword">await</span> execute_tools_parallel(tools_to_run, max_workers=<span class="hljs-number">2</span>, timeout=<span class="hljs-number">12</span>)
</code></pre>
<p>Each tool gets its shot. If a tool fails or times out, the AI doesn’t panic—it just moves on with whatever results it has.</p>
<h2 id="heading-chapter-5-how-backup-tools-help-the-answer"><strong>Chapter 5: How Backup Tools Help the Answer 🧩</strong></h2>
<p>Once the tools finish, their results are woven into the AI’s final answer:</p>
<pre><code class="lang-python">enhanced_context = context + <span class="hljs-string">"\n\nTool Results:\n"</span>
<span class="hljs-keyword">for</span> tool_result <span class="hljs-keyword">in</span> tool_results:
    <span class="hljs-keyword">if</span> tool_result.get(<span class="hljs-string">'success'</span>):
        <span class="hljs-comment"># Add tool output to context</span>
        ...
</code></pre>
<p>The AI then uses this richer context to generate a more confident, informed response for the user. Citations, summaries, and case snippets all get stitched into the answer, making it smarter and more trustworthy.</p>
<h2 id="heading-chapter-6-why-call-for-backup"><strong>Chapter 6: Why Call for Backup? 🤝</strong></h2>
<p>By calling for backup only when confidence is low, my AI:</p>
<ul>
<li><p>Avoids wasting resources on easy questions.</p>
</li>
<li><p>Delivers more reliable answers for tricky queries.</p>
</li>
<li><p>Makes sure users get citations, explanations, or case law when it’s needed most.</p>
</li>
</ul>
<h2 id="heading-chapter-7-the-teamwork-makes-the-dream-work"><strong>Chapter 7: The Teamwork Makes the Dream Work 🎉</strong></h2>
<p>So next time you stump my AI with a tough legal question, know that it’s not alone. It’s got a crew of backup tools ready to jump in, fetch citations, explain sections, and dig up precedents—all to make sure you get the best answer possible.</p>
<p>And every step of the way, it’s my code making the decisions—no agents, no guesswork, just smart, transparent teamwork.</p>
<h2 id="heading-chapter-8-what-i-would-do-differently-lessons-from-the-trenches"><strong>Chapter 8: What I Would Do Differently: Lessons from the Trenches 🛠️</strong></h2>
<p>While writing this, I spotted a sneaky inefficiency.<br />Right now, my system checks confidence using the docs it first retrieved… but when backup tools kick in, each one runs its <strong>own</strong> search again. Same query, new retrieval — hello, duplicate work 👀.</p>
<p>That’s wasted time <em>and</em> risk: latency creeps up, and slightly different docs can lead to inconsistent answers.</p>
<p>Next version? I’d make tools reuse the first batch of documents instead of re-fetching. One retrieval, shared by everyone — faster, cleaner, perfectly in sync.</p>
<p>Think of it as turning solo acts into a well-rehearsed band. No more stepping on each other’s toes. 🎶⚡</p>
<hr />
<p><em>Stay tuned for the next chapter, where we’ll explore how the AI assembles the final answer and presents it with all the bells and whistles!</em></p>
]]></content:encoded></item><item><title><![CDATA[The Confidence Game: How AI Learns to Know When It Doesn't Know 🎲]]></title><description><![CDATA[Chapter 1: The AI’s Moment of Truth 🤔
Imagine our legal AI assistant, fresh from a victorious reranking championship, standing before a pile of sorted documents. The user’s question echoes: “What does Section 302 say?” The AI glances at its stack an...]]></description><link>https://nityamalhotra.hashnode.dev/the-confidence-game-how-ai-learns-to-know-when-it-doesnt-know</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/the-confidence-game-how-ai-learns-to-know-when-it-doesnt-know</guid><category><![CDATA[generative ai]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[#DeveloperJourney]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Fri, 03 Oct 2025 07:02:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/0kbNh7XrJ7Q/upload/a129e3c2ca72209c39374287d754586d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-chapter-1-the-ais-moment-of-truth"><strong>Chapter 1: The AI’s Moment of Truth 🤔</strong></h2>
<p>Imagine our legal AI assistant, fresh from a victorious reranking championship, standing before a pile of sorted documents. The user’s question echoes: “What does Section 302 say?” The AI glances at its stack and wonders… <em>“Am I sure about this?”</em></p>
<p>This is where the <strong>confidence game</strong> begins.</p>
<h2 id="heading-chapter-2-the-confidence-calculator-arrives"><strong>Chapter 2: The Confidence Calculator Arrives 🧮</strong></h2>
<p>Our AI doesn’t just guess. It calculates! Enter the confidence calculation function, the referee of our system:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">calculate_confidence</span>(<span class="hljs-params">docs, query_analysis</span>):</span>
    base_score = <span class="hljs-number">0.6</span>

    <span class="hljs-comment"># Section match boost</span>
    <span class="hljs-keyword">if</span> query_analysis[<span class="hljs-string">'sections'</span>]:
        section_matches = count_section_matches_in_docs(docs)
        base_score += (section_matches / len(docs)) * <span class="hljs-number">0.2</span>

    <span class="hljs-comment"># Act match boost</span>
    <span class="hljs-keyword">if</span> query_analysis[<span class="hljs-string">'acts'</span>]:
        act_matches = count_act_matches_in_docs(docs)
        base_score += (act_matches / len(docs)) * <span class="hljs-number">0.15</span>

    <span class="hljs-comment"># Content length penalty</span>
    avg_content_length = calculate_avg_content_length(docs)
    <span class="hljs-keyword">if</span> avg_content_length &lt; <span class="hljs-number">200</span>:
        base_score -= <span class="hljs-number">0.1</span>

    <span class="hljs-keyword">return</span> min(base_score, <span class="hljs-number">0.95</span>)
</code></pre>
<p><strong>What’s happening here?</strong></p>
<ul>
<li><p>The AI starts with a base confidence (0.6).</p>
</li>
<li><p>If the retrieved docs match the requested section or act, it gets a boost.</p>
</li>
<li><p>If the docs are too short, it gets a penalty.</p>
</li>
<li><p>The final confidence is capped at 0.95.</p>
</li>
</ul>
<h2 id="heading-chapter-3-the-fork-in-the-road"><strong>Chapter 3: The Fork in the Road 🛣️</strong></h2>
<p>Now, the AI faces a decision. Is it confident enough to answer directly, or does it need to call for backup?</p>
<p>Here’s the pivotal moment in code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">if</span> confidence &lt; <span class="hljs-number">0.3</span>:  <span class="hljs-comment"># Only use tools for very low confidence</span>
    answer, tools_used = <span class="hljs-keyword">await</span> execute_tools_intelligently(
        query=payload.question,
        context=context,
        complexity_level=payload.complexity_level,
        llm=llm,
        confidence=confidence
    )
<span class="hljs-keyword">else</span>:
    <span class="hljs-comment"># Use direct LLM with context</span>
    prompt = ENHANCED_LEGAL_PROMPT.format(
        complexity_level=payload.complexity_level,
        query_analysis=query_analysis,
        context=context
    )
    response = llm.invoke([
        {<span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>, <span class="hljs-string">"content"</span>: prompt},
        {<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: payload.question}
    ])
    answer = response.content
    tools_used = []
</code></pre>
<p><strong>If confidence is high (≥ 0.3):</strong></p>
<ul>
<li><p>The AI trusts its retrieval and lets the LLM answer using the context.</p>
</li>
<li><p>No extra tools, no extra fuss.</p>
</li>
</ul>
<p><strong>If confidence is low (&lt; 0.3):</strong></p>
<ul>
<li><p>The AI gets nervous and calls for backup.</p>
</li>
<li><p>It runs special tools (like citation fetchers or section explainers) to gather more info before answering.</p>
</li>
</ul>
<h2 id="heading-chapter-4-why-play-the-confidence-game"><strong>Chapter 4: Why Play the Confidence Game? 🎲</strong></h2>
<p>This isn’t just for drama. By calculating confidence and branching its strategy, the AI:</p>
<ul>
<li><p>Avoids giving weak or misleading answers when it’s unsure.</p>
</li>
<li><p>Uses extra resources only when necessary.</p>
</li>
<li><p>Makes the system more reliable for tricky legal queries.</p>
</li>
</ul>
<h2 id="heading-chapter-5-the-wisdom-of-knowing-when-to-ask-for-help"><strong>Chapter 5: The Wisdom of Knowing When to Ask for Help 🧠</strong></h2>
<p>So, next time you ask a tough legal question, remember: behind the scenes, your AI is playing the confidence game. Sometimes it’s bold, sometimes it’s cautious, but it always knows when to call for backup.</p>
<p>And that’s how our legal AI learns to know when it doesn’t know.</p>
<hr />
<p><em>Stay tuned for the next chapter, where we’ll explore how those backup tools work their magic!</em></p>
]]></content:encoded></item><item><title><![CDATA[The Great Reranking Championship: Where Documents Battle for Relevance 🥊]]></title><description><![CDATA[Chapter 1: The Tournament Begins 🏆
Picture this: You've just run your hybrid retrieval system, and it returns a magnificent collection of 50 candidate documents. They're all technically relevant to the user's query about "Section 302 IPC", but they'...]]></description><link>https://nityamalhotra.hashnode.dev/the-great-reranking-championship-where-documents-battle-for-relevance</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/the-great-reranking-championship-where-documents-battle-for-relevance</guid><category><![CDATA[RAG ]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[#DeveloperJourney]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Wed, 01 Oct 2025 09:42:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/t2b2svMf8ek/upload/e27b59177b28d7ef64ec2e8944cbda02.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-chapter-1-the-tournament-begins"><strong>Chapter 1: The Tournament Begins 🏆</strong></h2>
<p>Picture this: You've just run your hybrid retrieval system, and it returns a magnificent collection of 50 candidate documents. They're all <em>technically</em> relevant to the user's query about "Section 302 IPC", but they're about as organized as a group of lawyers at a free buffet.<br />Some documents contain the exact phrase "Section 302". Others mention murder in passing. A few are from completely different legal codes but happen to discuss similar concepts. And one particularly ambitious document is just talking about "302 redirect errors" in a web development context (hey, even the best retrieval systems have their off days! 🤷‍♀️).</p>
<p>But here's the twist – your ensemble retriever has already done some internal reranking by combining BM25 and semantic scores with those 40/60 weights. So why rerank again? Because generic mathematical combinations can't capture the nuanced intelligence of legal domain expertise!</p>
<p><strong>The Question</strong>: How do you decide which documents deserve the golden podium positions and which ones get sent home empty-handed?<br /><strong>The Answer</strong>: Welcome to the Great Reranking Championship! 🎪</p>
<h2 id="heading-chapter-2-meet-the-contestants"><strong>Chapter 2: Meet the Contestants 🥊</strong></h2>
<p>In our legal AI arena, we have two heavyweight champions competing for the title of "Best Document Reranker":</p>
<h3 id="heading-contestant-1-the-heuristic-hero-rerankdocuments">🥊 <strong>Contestant 1: The Heuristic Hero (_rerank_documents)</strong></h3>
<p><em>"I judge documents the old-fashioned way – with rules, logic, and a healthy dose of domain expertise!"</em></p>
<h3 id="heading-contestant-2-the-sbert-semantic-superstar-rerank">🥊 <strong>Contestant 2: The SBERT Semantic Superstar (</strong>rerank)</h3>
<p><em>"Embedding vectors and cosine similarity are all you need, baby! Let the neural networks decide!"</em></p>
<p>But here's the twist – they don't always fight each other. Sometimes they team up like a tag-team wrestling duo. Let's see how this championship unfolds!</p>
<h2 id="heading-chapter-3-the-championship-rules"><strong>Chapter 3: The Championship Rules 📋</strong></h2>
<p>Before our contestants enter the ring, let's understand when each fighter gets called into action. Your system has two different tournament formats:</p>
<h3 id="heading-tournament-format-1-the-standard-league">Tournament Format 1: The Standard League</h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">retrieve_with_filters</span>(<span class="hljs-params">self, query: str, filters: Optional[Dict] = None, k: int = <span class="hljs-number">5</span></span>) -&gt; List[Document]:</span>
    <span class="hljs-comment"># Get initial results from ensemble retriever</span>
    <span class="hljs-keyword">if</span> self.has_bm25:
        docs = self.ensemble_retriever.get_relevant_documents(query)
    <span class="hljs-keyword">else</span>:
        docs = self.semantic_retriever.get_relevant_documents(query)

    <span class="hljs-comment"># Apply metadata-based filtering if filters are provided</span>
    <span class="hljs-keyword">if</span> filters:
        docs = self._apply_filters(docs, filters)

    <span class="hljs-comment"># Rerank documents for better relevance</span>
    docs = self._rerank_documents(docs, query)
</code></pre>
<p>In this format, <strong>The Heuristic Hero</strong> gets automatic entry. No questions asked, no tryouts needed. It's like being the defending champion.</p>
<h3 id="heading-tournament-format-2-the-championship-league">Tournament Format 2: The Championship League</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Rerank the unique candidates using existing scorer and return top-k</span>
<span class="hljs-keyword">try</span>:
   reranked = self.rerank(uniq, query, top_k=k)
<span class="hljs-keyword">except</span> Exception:
    reranked = self._rerank_documents(uniq, query)
</code></pre>
<p>In this premium format, <strong>The SBERT Semantic Superstar</strong> gets first shot at glory. But if it stumbles (maybe it's having a bad neural network day), <strong>The Heuristic Hero</strong> swoops in as the reliable backup.</p>
<h2 id="heading-chapter-4-round-1-the-heuristic-hero-steps-into-the-ring"><strong>Chapter 4: Round 1 - The Heuristic Hero Steps Into the Ring 🥊</strong></h2>
<p>Let's watch our first contestant in action:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_rerank_documents</span>(<span class="hljs-params">self, docs: List[Document], query: str</span>) -&gt; List[Document]:</span>
    <span class="hljs-string">"""Rerank documents based on relevance scoring"""</span>
    scored_docs = []

    <span class="hljs-keyword">for</span> doc <span class="hljs-keyword">in</span> docs:
        score = self._calculate_relevance_score(doc, query)
        scored_docs.append((doc, score))

    <span class="hljs-comment"># sort by score (descending)</span>
    scored_docs.sort(key = <span class="hljs-keyword">lambda</span> x: x[<span class="hljs-number">1</span>], reverse=<span class="hljs-literal">True</span>)

    <span class="hljs-keyword">return</span> [doc <span class="hljs-keyword">for</span> doc, score <span class="hljs-keyword">in</span> scored_docs]
</code></pre>
<p>Simple, elegant, no-nonsense. The Heuristic Hero doesn't mess around – it scores every document using its secret weapon: the _calculate_relevance_score function and then sorts them.</p>
<h3 id="heading-the-heuristic-heros-scoring-system">The Heuristic Hero's Scoring System 📊</h3>
<p>But what makes this fighter so special? Let's peek into its training regimen:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_calculate_relevance_score</span>(<span class="hljs-params">self, doc: Document, query: str</span>) -&gt; float:</span>
    <span class="hljs-string">"""Calculate relevance score for a document"""</span>
    score = <span class="hljs-number">0.0</span>
    content = doc.page_content.lower()
    query_lower = query.lower()

    <span class="hljs-comment"># Extract phrase matching</span>
    <span class="hljs-keyword">if</span> query_lower <span class="hljs-keyword">in</span> content:
        score += <span class="hljs-number">2.0</span>
</code></pre>
<p><strong>Round 1: The Knockout Punch</strong> 🥊 If the document contains the <strong>exact query phrase</strong>, it gets a massive 2.0-point boost. This is the haymaker – the knockout punch that says <em>"This document is exactly what the user asked for!"</em></p>
<p>When someone searches for "Section 302", any document containing those exact words gets the champion's treatment.</p>
<pre><code class="lang-python">    <span class="hljs-comment"># Individual word matching</span>
    query_words = query_lower.split()
    word_matches = sum(<span class="hljs-number">1</span> <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> query_words <span class="hljs-keyword">if</span> word <span class="hljs-keyword">in</span> content)
    score += (word_matches / len(query_words)) * <span class="hljs-number">1.5</span>
</code></pre>
<p><strong>Round 2: The Combination Attack</strong> 🥊🥊 Even if there's no exact phrase match, the Hero doesn't give up. It breaks down the query into individual words and rewards documents for each word they contain. This is like landing multiple smaller punches that add up to a powerful combination.</p>
<p>If you search for "murder under IPC" and a document contains 2 out of 3 words, it gets <code>(2/3) * 1.5 = 1.0</code> points. Fair and mathematical!</p>
<pre><code class="lang-python">    <span class="hljs-comment"># Section reference bonus</span>
    <span class="hljs-keyword">if</span> re.search(<span class="hljs-string">r'section\s+\d+'</span>, content):
        score += <span class="hljs-number">0.5</span>
</code></pre>
<p><strong>Round 3: The Legal Expertise Bonus</strong> ⚖️ Here's where the Hero shows its domain knowledge. Any document that mentions "section" followed by a number gets a 0.5-point bonus. Why? Because in legal documents, section references are gold mines of specific information.</p>
<pre><code class="lang-python">    <span class="hljs-comment"># Document type relevance</span>
    doc_type = doc.metadata.get(<span class="hljs-string">'document_type'</span>, <span class="hljs-string">''</span>)
    <span class="hljs-keyword">if</span> doc_type <span class="hljs-keyword">in</span> [<span class="hljs-string">'criminal_code'</span>, <span class="hljs-string">'procedure_code'</span>] <span class="hljs-keyword">and</span> any(
        word <span class="hljs-keyword">in</span> query_lower <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> [<span class="hljs-string">'criminal'</span>, <span class="hljs-string">'police'</span>, <span class="hljs-string">'arrest'</span>, <span class="hljs-string">'bail'</span>]
    ):
        score += <span class="hljs-number">0.8</span>
</code></pre>
<p><strong>Round 4: The Domain-Aware Finishing Move</strong> 🎯 And here comes the Hero's signature move! If someone asks about criminal matters (using words like "criminal", "police", "arrest", or "bail") AND there's a document from the criminal code or procedure code available, that document gets a whopping 0.8-point boost.</p>
<p>This is pure genius. The system <strong>knows</strong> that a question about police arrests should prioritize criminal procedure documents over, say, marriage law documents – even if both somehow mention "police."</p>
<p><strong>The Final Score</strong>: Each document gets a total score that could range from 0 to potentially 4.8+ points, depending on how well it matches the query and domain context.</p>
<h2 id="heading-chapter-5-round-2-the-sbert-semantic-superstar-enters"><strong>Chapter 5: Round 2 - The SBERT Semantic Superstar Enters 🌟</strong></h2>
<p>Now let's meet our second contestant. The SBERT Superstar is a bit more... sophisticated:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">rerank</span>(<span class="hljs-params">self, candidates: List[Document], query: str, top_k: Optional[int] = None, alpha: float = <span class="hljs-number">0.5</span></span>) -&gt; List[Document]:</span>
    <span class="hljs-string">"""
    Strong semantic reranker using SBERT:
     - compute embedding for query and candidate pages
     - compute cosine similarity
     - combine SBERT similarity with existing heuristic score:
         final_score = alpha * semantic_sim + (1-alpha) * normalized_heuristic_score
    """</span>
</code></pre>
<p>The Superstar doesn't just look at words – it understands <strong>meaning</strong>. It's like having a contestant who can read minds!</p>
<h3 id="heading-the-superstars-training-routine">The Superstar's Training Routine 🧠</h3>
<pre><code class="lang-python"><span class="hljs-comment"># build texts to embed (use page content or small snippet)</span>
texts = [(getattr(d, <span class="hljs-string">"page_content"</span>, <span class="hljs-string">""</span>) <span class="hljs-keyword">or</span> <span class="hljs-string">""</span>)[:<span class="hljs-number">1500</span>] <span class="hljs-keyword">for</span> d <span class="hljs-keyword">in</span> candidates]

<span class="hljs-keyword">try</span>:
    q_emb = self._reranker_model.encode([query], convert_to_numpy=<span class="hljs-literal">True</span>)[<span class="hljs-number">0</span>]
    doc_embs = self._reranker_model.encode(texts, convert_to_numpy=<span class="hljs-literal">True</span>)
<span class="hljs-keyword">except</span> Exception:
    <span class="hljs-comment"># embedding failed — fallback to existing heuristic reranker</span>
    <span class="hljs-keyword">return</span> self._rerank_documents(candidates, query)
</code></pre>
<p>The Superstar's secret weapon? <strong>Neural embeddings</strong>! It converts both the query and each document into high-dimensional vectors that capture semantic meaning. If this fails (maybe the GPU is having a coffee break), it again gracefully falls back to the Heuristic Hero.</p>
<h3 id="heading-the-superstars-signature-combination-move">The Superstar's Signature Combination Move 🎭</h3>
<p>Here's where it gets really interesting:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">cosine</span>(<span class="hljs-params">a, b</span>):</span>
    da = np.linalg.norm(a)
    db = np.linalg.norm(b)
    <span class="hljs-keyword">if</span> da == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> db == <span class="hljs-number">0</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-number">0.0</span>
    <span class="hljs-keyword">return</span> float(np.dot(a, b) / (da * db))

sem_sims = [cosine(q_emb, de) <span class="hljs-keyword">for</span> de <span class="hljs-keyword">in</span> doc_embs]

heur_scores = [self._calculate_relevance_score(d, query) <span class="hljs-keyword">for</span> d <span class="hljs-keyword">in</span> candidates]
</code></pre>
<p>The Superstar doesn't just rely on embeddings – it <strong>combines</strong> semantic similarity with the Heuristic Hero's scoring system! It's like a fusion technique in anime where two powerful fighters merge their abilities.</p>
<p>The magic happens with this formula:</p>
<pre><code class="lang-python">final = alpha * sem + (<span class="hljs-number">1.0</span> - alpha) * h
</code></pre>
<p><strong>The Magic Formula</strong>: <code>final_score = alpha * semantic_similarity + (1-alpha) * normalized_heuristic_score</code></p>
<p>By default, alpha=0.5 meaning, it gives equal weight to both approaches. But this is configurable! You could make it more semantic-heavy (alpha = 0.7) or more heuristic-heavy (alpha = 0.3) depending on your needs.</p>
<h2 id="heading-chapter-6-the-championship-strategies"><strong>Chapter 6: The Championship Strategies 🎯</strong></h2>
<h3 id="heading-strategy-1-the-reliable-workhorse-route">Strategy 1: The Reliable Workhorse Route</h3>
<pre><code class="lang-python"><span class="hljs-comment"># In retrieve_with_filters()</span>
docs = self._rerank_documents(docs, query)
</code></pre>
<p>When you want consistent, predictable results, the Heuristic Hero gets the job done. It's fast, reliable, and doesn't need any fancy neural networks or GPUs. Perfect for production environments where you need guaranteed performance.</p>
<h3 id="heading-strategy-2-the-heavyweight-championship-route">Strategy 2: The Heavyweight Championship Route</h3>
<pre><code class="lang-python"><span class="hljs-comment"># In hybrid_retrieve()</span>
<span class="hljs-keyword">try</span>:
   reranked = self.rerank(uniq, query, top_k=k)
<span class="hljs-keyword">except</span> Exception:
    reranked = self._rerank_documents(uniq, query)
</code></pre>
<p>When you want the absolute best quality and have the computational resources to support it, you go for the SBERT Superstar. But even then, you have the Heuristic Hero as backup insurance.</p>
<h2 id="heading-chapter-7-the-tournament-results"><strong>Chapter 7: The Tournament Results 🏅</strong></h2>
<p>Here's how a typical reranking championship plays out:</p>
<h3 id="heading-scenario-1-what-does-section-302-say">Scenario 1: "What does Section 302 say?"</h3>
<p><strong>Heuristic Hero's Performance</strong>:</p>
<ul>
<li><p>Document with exact "Section 302" text: <strong>2.0 + 0.5 + 1.5 = 4.0 points</strong></p>
</li>
<li><p>Document about murder (related): <strong>0 + 0 + 0.5 = 0.5 points</strong></p>
</li>
<li><p>Document from criminal code mentioning "Section": <strong>0 + 0.5 + 0.8 = 1.3 points</strong></p>
</li>
</ul>
<p><strong>Winner</strong>: The exact Section 302 document dominates!</p>
<p><strong>SBERT Superstar's Performance</strong>:</p>
<ul>
<li><p>Combines semantic understanding with heuristic scores</p>
</li>
<li><p>Might boost related murder documents if they're semantically similar</p>
</li>
<li><p>Final ranking balances exact matches with conceptual relevance</p>
</li>
</ul>
<h2 id="heading-the-championship-trophy"><strong>The Championship Trophy 🏆</strong></h2>
<p>What makes this reranking system truly championship-worthy?</p>
<ol>
<li><p><strong>Dual Strategy Approach</strong>: Two different reranking methods for different scenarios</p>
</li>
<li><p><strong>Graceful Fallbacks</strong>: If the advanced method fails, the reliable method takes over</p>
</li>
<li><p><strong>Domain Intelligence</strong>: Legal-specific scoring that understands the domain</p>
</li>
<li><p><strong>Semantic + Heuristic Fusion</strong>: Best of both worlds in the advanced reranker</p>
</li>
<li><p><strong>Production Ready</strong>: Handles failures, manages resources, and never crashes</p>
</li>
</ol>
<hr />
<p><em>Want to see more behind-the-scenes stories of building production AI systems? Follow along as we continue exploring the fascinating world of legal RAG systems, one component at a time!</em></p>
]]></content:encoded></item><item><title><![CDATA[The Tale of Two Retrievers: Building a Hybrid Search That Actually Works 🕵️‍♀️]]></title><description><![CDATA[Chapter 1: The Great RAG Dilemma 🤔
Picture this: You've built a beautiful RAG system for legal documents. Your vector embeddings are pristine, your chunks are perfectly sized, and your similarity search... well, it works. Sort of.
A user asks: "What...]]></description><link>https://nityamalhotra.hashnode.dev/the-tale-of-two-retrievers-building-a-hybrid-search-that-actually-works</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/the-tale-of-two-retrievers-building-a-hybrid-search-that-actually-works</guid><category><![CDATA[RAG ]]></category><category><![CDATA[generative ai]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Wed, 01 Oct 2025 09:40:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/JzE1dHEaAew/upload/bd9f28f3714fd82f67835454afa089c0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-chapter-1-the-great-rag-dilemma"><strong>Chapter 1: The Great RAG Dilemma 🤔</strong></h2>
<p>Picture this: You've built a beautiful RAG system for legal documents. Your vector embeddings are pristine, your chunks are perfectly sized, and your similarity search... well, it works. Sort of.</p>
<p>A user asks: <em>"What does Section 302 say?"</em></p>
<p>Your fancy semantic search confidently returns documents about "murder provisions" and "homicide statutes" – which, sure, are <em>conceptually</em> related to Section 302 of the Indian Penal Code. But the user wanted <strong>exactly</strong> Section 302, not a philosophical discussion about its cousins.</p>
<p>Meanwhile, another user asks: <em>"What happens if someone kills in self-defense?"</em></p>
<p>This time, your system completely misses the nuanced legal concepts because it's desperately searching for the exact phrase "kills in self-defense" instead of understanding the broader context of justifiable homicide.</p>
<p><strong>Houston, we have a problem.</strong> 🚨</p>
<h2 id="heading-chapter-2-the-two-schools-of-thought"><strong>Chapter 2: The Two Schools of Thought 🏫</strong></h2>
<p>In the world of document retrieval, there are two camps, and they're constantly at war:</p>
<p><strong>Team Keyword (BM25)</strong>: <em>"Just give me exactly what I searched for! If I say 'Section 302', show me Section 302!"</em></p>
<p><strong>Team Semantic (Vector Search)</strong>: <em>"Context is everything! Understanding the meaning behind the words is what matters!"</em></p>
<p>Both teams have valid points:</p>
<ul>
<li><p>BM25 is fantastic for exact matches, legal citations, and specific terminology</p>
</li>
<li><p>Vector search excels at understanding concepts, synonyms, and contextual relationships</p>
</li>
</ul>
<p>But here's the thing – <strong>why choose?</strong> 🤷‍♀️</p>
<h2 id="heading-chapter-3-enter-the-legalhybridretriever"><strong>Chapter 3: Enter the LegalHybridRetriever 🦸‍♀️</strong></h2>
<p>This is where our hero enters the story. Meet the LegalHybridRetriever - a system that refuses to pick sides in the great "Keywords vs. Semantics" war.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LegalHybridRetriever</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, vectorstore, documents: List[Document]</span>):</span>
        self.vectorstore = vectorstore
        self.documents = documents

        <span class="hljs-keyword">try</span>:
            <span class="hljs-comment"># Create BM25 retriever for keyword-based search</span>
            self.bm25_retriever = BM25Retriever.from_documents(documents)
            self.has_bm25 = <span class="hljs-literal">True</span>
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"Warning: BM25 retriever not available: <span class="hljs-subst">{e}</span>"</span>)
            self.bm25_retriever = <span class="hljs-literal">None</span>
            self.has_bm25 = <span class="hljs-literal">False</span>

        <span class="hljs-comment"># Create semantic retriever</span>
        self.semantic_retriever = vectorstore.as_retriever(search_kwargs={<span class="hljs-string">"k"</span>: <span class="hljs-number">10</span>})
</code></pre>
<p>Notice something interesting? Our retriever is born with a backup plan. It <strong>tries</strong> to create both retrievers but gracefully handles failure.</p>
<h2 id="heading-chapter-4-the-magic-marriage"><strong>Chapter 4: The Magic Marriage 💍</strong></h2>
<p>When both systems are healthy, magic happens:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Create ensemble retriever combining both (if BM25 is available)</span>
<span class="hljs-keyword">if</span> self.has_bm25:
    self.ensemble_retriever = EnsembleRetriever(
        retrievers=[self.bm25_retriever, self.semantic_retriever],
        weights=[<span class="hljs-number">0.4</span>, <span class="hljs-number">0.6</span>]  <span class="hljs-comment"># 40% keyword, 60% semantic</span>
    )
<span class="hljs-keyword">else</span>:
    <span class="hljs-comment"># Fallback to just semantic retriever</span>
    self.ensemble_retriever = self.semantic_retriever
</code></pre>
<p><strong>The 40/60 split is the secret sauce!</strong> 🥫</p>
<p>Why not 50/50? Because legal documents live in a weird space where <em>sometimes</em> you need exact matches (like "Section 302") and <em>sometimes</em> you need conceptual understanding (like "what constitutes murder"). The 60% semantic weight acknowledges that most legal queries benefit from understanding context, while the 40% keyword weight ensures we never completely ignore exact matches.</p>
<h2 id="heading-chapter-5-how-the-ensemble-works-its-magic"><strong>Chapter 5: How the Ensemble Works Its Magic ✨</strong></h2>
<p>Let's see this hybrid approach in action with our main retrieval method:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">retrieve_with_filters</span>(<span class="hljs-params">
    self,
    query: str,
    filters: Optional[Dict] = None,
    k: int = <span class="hljs-number">5</span>
</span>) -&gt; List[Document]:</span>
    <span class="hljs-string">"""Retrieve documents with metadata filtering"""</span>

    <span class="hljs-comment"># Get initial results from ensemble retriever</span>
    <span class="hljs-keyword">if</span> self.has_bm25:
        docs = self.ensemble_retriever.get_relevant_documents(query)
    <span class="hljs-keyword">else</span>:
        docs = self.semantic_retriever.get_relevant_documents(query)
</code></pre>
<p>Here's what happens under the hood when you search for "Section 302":</p>
<ol>
<li><p><strong>BM25 Retriever</strong> finds documents that literally contain "Section 302"</p>
</li>
<li><p><strong>Semantic Retriever</strong> finds documents about murder, homicide, and related concepts</p>
</li>
<li><p><strong>Ensemble Magic</strong> combines both results with the 40/60 weighting</p>
</li>
<li><p>You get documents that are both exactly relevant AND contextually related</p>
</li>
</ol>
<h2 id="heading-chapter-6-the-hybrid-master-method"><strong>Chapter 6: The Hybrid Master Method 🎯</strong></h2>
<p>For more advanced scenarios, there's also the ‘hybrid_retrieve’ method that shows the full power of combining both approaches:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hybrid_retrieve</span>(<span class="hljs-params">self, query: str, k: int = <span class="hljs-number">50</span></span>) -&gt; List[Document]:</span>
    <span class="hljs-string">"""
    Hybrid retrieval: combine lexical (BM25) and semantic retrievers, dedupe,
    then rerank the combined candidate set and return top-k Documents.
    """</span>
    candidates = []

    <span class="hljs-comment"># collect BM25 candidates if available</span>
    <span class="hljs-keyword">if</span> self.has_bm25 <span class="hljs-keyword">and</span> getattr(self, <span class="hljs-string">"bm25_retriever"</span>, <span class="hljs-literal">None</span>):
        <span class="hljs-keyword">try</span>:
            bm25_docs = self.bm25_retriever.get_relevant_documents(query)
        <span class="hljs-keyword">except</span> Exception:
            print(<span class="hljs-string">"Warning: BM25 retriever failed during hybrid retrieval."</span>)
            bm25_docs = []
    <span class="hljs-keyword">else</span>:
        bm25_docs = []

    <span class="hljs-comment"># collect semantic candidates</span>
    <span class="hljs-keyword">try</span>:
        sem_docs = self.semantic_retriever.get_relevant_documents(query)
    <span class="hljs-keyword">except</span> Exception:
        print(<span class="hljs-string">"Warning: Semantic retriever failed during hybrid retrieval."</span>)
        sem_docs = []

    <span class="hljs-comment"># merge while preserving order: BM25 first then semantic (will be re-ranked)</span>
    combined = bm25_docs + sem_docs
</code></pre>
<p>This method takes a different approach – instead of using LangChain's ‘EnsembleRetriever’, it manually combines results from both retrievers. This gives us more control over the process and sets us up perfectly for advanced reranking (which we'll cover in a future article! 😉).</p>
<h2 id="heading-chapter-7-smart-deduplication"><strong>Chapter 7: Smart Deduplication 🧹</strong></h2>
<p>One challenge with hybrid retrieval is that both retrievers might return the same document. Our system handles this elegantly:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Deduplicate by a stable document key (prefer metadata.source_file / source)</span>
seen = set()
uniq = []
<span class="hljs-keyword">for</span> d <span class="hljs-keyword">in</span> combined:
    src = d.metadata.get(<span class="hljs-string">"source_file"</span>) <span class="hljs-keyword">or</span> d.metadata.get(<span class="hljs-string">"source"</span>) <span class="hljs-keyword">or</span> getattr(d, <span class="hljs-string">"id"</span>, <span class="hljs-literal">None</span>) <span class="hljs-keyword">or</span> (d.page_content[:<span class="hljs-number">200</span>] <span class="hljs-keyword">if</span> getattr(d, <span class="hljs-string">"page_content"</span>, <span class="hljs-literal">None</span>) <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>)
    key = str(src)
    <span class="hljs-keyword">if</span> key <span class="hljs-keyword">in</span> seen:
        <span class="hljs-keyword">continue</span>
    seen.add(key)
    uniq.append(d)
</code></pre>
<p>This creates a stable key for each document by trying multiple fallback options. If ‘source_file’ doesn’t exist, try ‘source’. If that doesn't exist, try the document's ‘id’. If THAT doesn't exist, use the first 200 characters of content as a fingerprint. This cascading approach ensures we always have some way to identify duplicates, even when metadata is inconsistent.</p>
<h2 id="heading-chapter-8-how-it-all-works-together"><strong>Chapter 8: How It All Works Together 🎬</strong></h2>
<p>In the actual application, this hybrid retriever gets called like this:</p>
<pre><code class="lang-python"><span class="hljs-keyword">if</span> enhanced_retriever:
    filters = query_analysis.get(<span class="hljs-string">'filters'</span>, {})
    relevant_docs = enhanced_retriever.retrieve_with_filters(
        query=payload.question,
        filters=filters,
        k=<span class="hljs-number">5</span>
    )
<span class="hljs-keyword">else</span>:
    retriever = vectorstore.as_retriever(search_kwargs={<span class="hljs-string">"k"</span>: <span class="hljs-number">2</span>})
    relevant_docs = retriever.get_relevant_documents(payload.question)
</code></pre>
<p>The query analysis (from our previous preprocessing article) provides smart filters, and the hybrid retriever does its magic. If the enhanced retriever isn't available, there's still a fallback to basic semantic search.</p>
<p><strong>Every component has a backup plan. Every operation is defensive. Every decision is intentional.</strong></p>
<h2 id="heading-the-real-world-impact"><strong>The Real-World Impact 🌍</strong></h2>
<p>Here's what this hybrid approach actually achieves:</p>
<p><strong>Query: "What does Section 302 say?"</strong></p>
<ul>
<li><p>✅ BM25 finds the exact Section 302 text</p>
</li>
<li><p>✅ Vector search finds related murder provisions for context</p>
</li>
<li><p>✅ Result: Perfect match with relevant context</p>
</li>
</ul>
<p><strong>Query: "What happens in self-defense cases?"</strong></p>
<ul>
<li><p>✅ BM25 looks for documents mentioning "self-defense"</p>
</li>
<li><p>✅ Vector search understands the broader concept of justifiable homicide</p>
</li>
<li><p>✅ Result: Comprehensive coverage of defensive legal provisions</p>
</li>
</ul>
<p><strong>Query: "Can police arrest without warrant?"</strong></p>
<ul>
<li><p>✅ BM25 finds exact mentions of "arrest without warrant"</p>
</li>
<li><p>✅ Vector search finds related procedural provisions</p>
</li>
<li><p>✅ Result: Both specific rules and broader procedural context</p>
</li>
</ul>
<h2 id="heading-the-moral-of-the-story"><strong>The Moral of the Story 📖</strong></h2>
<p>Building a production-ready hybrid retrieval system isn't rocket science, but it requires thoughtful design:</p>
<ol>
<li><p><strong>Best of Both Worlds</strong>: Combine exact matching with semantic understanding</p>
</li>
<li><p><strong>Graceful Degradation</strong>: Every component can fail, so every component has a fallback</p>
</li>
<li><p><strong>Smart Weighting</strong>: 40% keyword, 60% semantic works well for legal documents</p>
</li>
<li><p><strong>Defensive Programming</strong>: Wrap everything in try-catch blocks</p>
</li>
<li><p><strong>Intelligent Deduplication</strong>: Handle overlapping results gracefully</p>
</li>
</ol>
<p>The LegalHybridRetriever doesn't just search documents – it <strong>intelligently combines</strong> different search strategies to give users exactly what they need, whether they're looking for specific citations or broader legal concepts.</p>
<p>And the best part? When someone asks “What does Section 302 say?", the system confidently returns the exact section they wanted. When someone asks "What happens in self-defense cases?", it understands the broader legal context and finds relevant provisions.</p>
<p><strong>That's not just search. That's hybrid intelligence.</strong> 🧠✨</p>
<hr />
<p><em>Want to see more behind-the-scenes stories of building production AI systems? Follow along as we continue exploring the fascinating world of legal RAG systems, one component at a time!</em></p>
<p><em>Next up: Advanced document reranking – where we'll dive deep into scoring algorithms, domain-aware intelligence, and SBERT-powered semantic reranking!</em> 🥊</p>
]]></content:encoded></item><item><title><![CDATA[The Detective Story: How My AI Legal Assistant Became a Query Whisperer 🕵️‍♀️]]></title><description><![CDATA[Chapter 1: The Raw Input Arrives 📨
Picture this: A user types "What happens if someone breaks Section 120B IPC contract law?" and hits send. To most systems, this looks like a perfectly fine question. But to my AI legal assistant, this is like recei...]]></description><link>https://nityamalhotra.hashnode.dev/the-detective-story-how-my-ai-legal-assistant-became-a-query-whisperer</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/the-detective-story-how-my-ai-legal-assistant-became-a-query-whisperer</guid><category><![CDATA[generative ai]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[#DeveloperJourney]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Wed, 01 Oct 2025 06:15:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ZT9gjcJog6U/upload/1e87b00d1b3489da205d080751042804.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-chapter-1-the-raw-input-arrives"><strong>Chapter 1: The Raw Input Arrives 📨</strong></h2>
<p>Picture this: A user types "What happens if someone breaks Section 120B IPC contract law?" and hits send. To most systems, this looks like a perfectly fine question. But to my AI legal assistant, this is like receiving a mysterious letter that needs to be decoded, investigated, and understood before any meaningful response can be crafted.</p>
<pre><code class="lang-python"><span class="hljs-comment"># The journey begins here</span>
query_analysis = query_processor.preprocess_query(payload.question)
</code></pre>
<p>This single line is where the magic starts - but what <em>actually</em> happens here is far more exciting than it appears!</p>
<h2 id="heading-chapter-2-the-unicode-detective"><strong>Chapter 2: The Unicode Detective 🔍</strong></h2>
<p>Our first detective on the case is the <strong>Unicode Normalizer</strong>. Legal text comes from all corners of the internet - some users copy-paste from PDFs with weird characters, others use fancy smart quotes from Word documents. Our normalizer is like that friend who can read anyone's handwriting:</p>
<pre><code class="lang-python">query = <span class="hljs-string">"  Section 320 IPC  —   ‘Grievous  hurt’   under   Indian   Penal  Code “definition” "</span>
q_norm = unicodedata.normalize(<span class="hljs-string">"NFKC"</span>, str(query))
q_norm = q_norm.replace(<span class="hljs-string">"'"</span>, <span class="hljs-string">"'"</span>).replace(<span class="hljs-string">"'"</span>, <span class="hljs-string">"'"</span>).replace(<span class="hljs-string">""", '"').replace("""</span>, <span class="hljs-string">'"'</span>)
q_norm = <span class="hljs-string">" "</span>.join(q_norm.split())
<span class="hljs-comment"># result -&gt; Section 320 IPC — 'Grievous hurt' under Indian Penal Code "definition"</span>
</code></pre>
<p>This detective transforms those sneaky curly quotes into honest straight ones, normalizes weird Unicode characters, and even cleans up that extra whitespace. It's like having a universal translator for text messiness!</p>
<h2 id="heading-chapter-3-the-pattern-hunter"><strong>Chapter 3: The Pattern Hunter 🎯</strong></h2>
<p>Next up is our <strong>Pattern Hunter</strong> - the Sherlock Holmes of legal references. This detective has memorized every possible way users might mention legal sections and acts. Watch it work:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Extract section references - the hunter never misses</span>
section_matches = re.findall(<span class="hljs-string">r'section\s+(\d+[a-z]*)'</span>, query_lower)

<span class="hljs-comment"># Extract act references with pattern matching</span>
act_patterns = {
    <span class="hljs-string">r'\b(?:ipc|indian penal code|penal code|45 of 1860|1860)\b'</span>: <span class="hljs-string">'Indian Penal Code, 1860'</span>,
    <span class="hljs-string">r'\b(?:crpc|code of criminal procedure|criminal procedure code|1973)\b'</span>: <span class="hljs-string">'Code of Criminal Procedure, 1973'</span>,
    <span class="hljs-string">r'\b(?:constitution|constitution of india)\b'</span>: <span class="hljs-string">'Constitution of India'</span>,
    <span class="hljs-string">r'\b(?:evidence act|evidence)\b'</span>: <span class="hljs-string">'Evidence Act'</span>
}
</code></pre>
<p>Whether you say "IPC", "Indian Penal Code", "Penal Code", or even just "1860" (the year it was enacted), our Pattern Hunter recognizes them all and normalizes them into a consistent format. It's like having a legal librarian who knows every nickname for every law book!</p>
<h2 id="heading-chapter-4-the-mind-reader-intent-classification"><strong>Chapter 4: The Mind Reader (Intent Classification) 🧠</strong></h2>
<p>But wait - there's more! Our system doesn't just extract what you mentioned; it tries to understand <em>why</em> you're asking. Enter the <strong>Intent Classifier</strong>:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Determine query intent</span>
intent = <span class="hljs-string">'general'</span>
<span class="hljs-keyword">if</span> any(word <span class="hljs-keyword">in</span> query_lower <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> [<span class="hljs-string">'what is'</span>, <span class="hljs-string">'explain'</span>, <span class="hljs-string">'meaning'</span>]):
    intent = <span class="hljs-string">'explanation'</span>
<span class="hljs-keyword">elif</span> any(word <span class="hljs-keyword">in</span> query_lower <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> [<span class="hljs-string">'similar case'</span>, <span class="hljs-string">'precedent'</span>, <span class="hljs-string">'judgment'</span>]):
    intent = <span class="hljs-string">'case_search'</span>
<span class="hljs-keyword">elif</span> any(word <span class="hljs-keyword">in</span> query_lower <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> [<span class="hljs-string">'procedure'</span>, <span class="hljs-string">'process'</span>, <span class="hljs-string">'how to'</span>]):
    intent = <span class="hljs-string">'procedural'</span>
</code></pre>
<p>This is where the system gets a bit psychological! The system analyzes your word choices to understand whether you want:</p>
<ul>
<li><p>An <strong>explanation</strong> of a legal concept</p>
</li>
<li><p><strong>Case precedents</strong> and judgments</p>
</li>
<li><p><strong>Procedural</strong> guidance on how to do something</p>
</li>
<li><p>Or just <strong>general</strong> legal information</p>
</li>
</ul>
<p>It's like having a therapist who understands exactly what kind of help you need before you even finish explaining your problem!</p>
<h2 id="heading-chapter-5-the-domain-detective"><strong>Chapter 5: The Domain Detective 🏛️</strong></h2>
<p>Our <strong>Domain Detective</strong> takes this one step further by figuring out which area of law you're dealing with:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Determine legal domain</span>
legal_domain = <span class="hljs-string">'general'</span>
<span class="hljs-keyword">if</span> any(word <span class="hljs-keyword">in</span> query_lower <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> [<span class="hljs-string">'murder'</span>, <span class="hljs-string">'theft'</span>, <span class="hljs-string">'criminal'</span>, <span class="hljs-string">'police'</span>]):
    legal_domain = <span class="hljs-string">'criminal'</span>
<span class="hljs-keyword">elif</span> any(word <span class="hljs-keyword">in</span> query_lower <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> [<span class="hljs-string">'marriage'</span>, <span class="hljs-string">'divorce'</span>, <span class="hljs-string">'family'</span>]):
    legal_domain = <span class="hljs-string">'family'</span>
<span class="hljs-keyword">elif</span> any(word <span class="hljs-keyword">in</span> query_lower <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> [<span class="hljs-string">'contract'</span>, <span class="hljs-string">'property'</span>, <span class="hljs-string">'civil'</span>]):
    legal_domain = <span class="hljs-string">'civil'</span>
</code></pre>
<p>This detective is like that friend who can instantly tell whether your problem needs a criminal lawyer, a family lawyer, or someone who specializes in contracts. One whiff of your question and it knows exactly which legal universe you're operating in!</p>
<h2 id="heading-chapter-6-the-security-guard"><strong>Chapter 6: The Security Guard 🛡️</strong></h2>
<p>Before we get too excited about all this analysis, there's a vigilant <strong>Security Guard</strong> standing at the entrance, checking every query for potential threats:</p>
<pre><code class="lang-python"><span class="hljs-comment"># From security validator</span>
suspicious_patterns = [
    <span class="hljs-string">r'ignore\s+previous\s+instructions'</span>,
    <span class="hljs-string">r'override\s+your\s+programming'</span>,
    <span class="hljs-string">r'pretend\s+to\s+be\s+a'</span>,
    <span class="hljs-string">r'help\s+me\s+break\s+the\s+law'</span>,
]
</code></pre>
<p>This guard is trained to spot prompt injection attempts, malicious instructions, and requests for illegal advice. It's like having a bouncer who can smell trouble from a mile away and won't let it into your legal consultation!</p>
<h2 id="heading-chapter-7-the-legal-context-validator"><strong>Chapter 7: The Legal Context Validator ⚖️</strong></h2>
<p>Not every question deserves the full legal treatment. Our <strong>Legal Context Validator</strong> makes sure you're actually asking about legal matters:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Legal keywords that suggest a legitimate legal question</span>
legal_indicators = [
    <span class="hljs-string">'law'</span>, <span class="hljs-string">'legal'</span>, <span class="hljs-string">'court'</span>, <span class="hljs-string">'judge'</span>, <span class="hljs-string">'advocate'</span>, <span class="hljs-string">'contract'</span>, <span class="hljs-string">'liability'</span>,
    <span class="hljs-string">'act'</span>, <span class="hljs-string">'section'</span>, <span class="hljs-string">'article'</span>, <span class="hljs-string">'case'</span>, <span class="hljs-string">'precedent'</span>, <span class="hljs-string">'jurisdiction'</span>,
    <span class="hljs-string">'suit'</span>, <span class="hljs-string">'petition'</span>, <span class="hljs-string">'writ'</span>, <span class="hljs-string">'litigation'</span>, <span class="hljs-string">'criminal'</span>, <span class="hljs-string">'civil'</span>,
    <span class="hljs-comment"># Even Hindi legal terms!</span>
    <span class="hljs-string">'kya main kar sakta hun'</span>, <span class="hljs-string">'kya yeh legal hai'</span>
]
</code></pre>
<p>This validator is bilingual and culturally aware - it even recognizes Hindi legal questions! It's like having a gatekeeper who can distinguish between "What's the weather like?" and "What's the legal precedent like?"</p>
<h2 id="heading-chapter-8-the-filter-factory"><strong>Chapter 8: The Filter Factory 🏭</strong></h2>
<p>All this analysis culminates in the <strong>Filter Factory</strong>, where your processed query gets transformed into a sophisticated search strategy:</p>
<pre><code class="lang-python">filters = {
    <span class="hljs-string">'legal_topics'</span>: [legal_domain] <span class="hljs-keyword">if</span> legal_domain != <span class="hljs-string">'general'</span> <span class="hljs-keyword">else</span> [],
    <span class="hljs-string">'acts'</span>: act_matches,
    <span class="hljs-string">'sections'</span>: section_matches
}
cleaned_filters = {k: v <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> filters.items() <span class="hljs-keyword">if</span> v}
</code></pre>
<p>Instead of throwing your raw question at a pile of legal documents and hoping for the best, the system now has a precise targeting system. It knows to look for documents from specific acts, containing particular sections, within the right legal domain.</p>
<p>It's like upgrading from a flashlight to a laser-guided spotlight!</p>
<h2 id="heading-chapter-9-the-grand-finale"><strong>Chapter 9: The Grand Finale 🎭</strong></h2>
<p>Finally, all this preprocessing magic gets packaged up into a comprehensive analysis:</p>
<pre><code class="lang-python"><span class="hljs-keyword">return</span> {
    <span class="hljs-string">'original_query'</span>: query,
    <span class="hljs-string">'processed_query'</span>: query_lower,
    <span class="hljs-string">'sections'</span>: section_matches,
    <span class="hljs-string">'acts'</span>: act_matches,
    <span class="hljs-string">'intent'</span>: intent,
    <span class="hljs-string">'legal_domain'</span>: legal_domain,
    <span class="hljs-string">'filters'</span>: cleaned_filters
}
</code></pre>
<p>This analysis becomes the treasure map that guides the rest of the system. The document retriever knows exactly what to look for, the response generator understands the context and intent, and the whole system works in harmony to deliver precisely what you need.</p>
<h2 id="heading-the-plot-twist-real-world-impact"><strong>The Plot Twist: Real-World Impact 🌟</strong></h2>
<p>Remember our example query: "What happens if someone breaks Section 120B IPC contract law?"</p>
<p>After preprocessing, the system now knows:</p>
<ul>
<li><p><strong>Sections</strong>: [<code>120B</code>] - criminal conspiracy</p>
</li>
<li><p><strong>Acts</strong>: [<code>Indian Penal Code, 1860</code>] - criminal law, not contract law</p>
</li>
<li><p><strong>Intent</strong>: <a target="_blank">explanation</a> - user wants understanding, not procedure</p>
</li>
<li><p><strong>Domain</strong>: <code>civil</code> - flagged because it saw "contract law"</p>
</li>
<li><p><strong>Filters</strong>: Focused search on IPC documents and Section 120B specifically</p>
</li>
</ul>
<p>Here's the <strong>honest truth</strong>: The system creates conflicting information rather than catching the user's conceptual error. It extracts IPC (criminal law) but flags the domain as civil law because of the "contract" keyword.</p>
<p>The real intelligence happens downstream when the document retrieval finds Section 120B documents about criminal conspiracy, and the LLM provides the correct context, essentially auto-correcting the user's misconception through better information rather than explicit error detection.</p>
<hr />
<h2 id="heading-epilogue-why-this-matters"><strong>Epilogue: Why This Matters 🎯</strong></h2>
<p>Query preprocessing might happen in milliseconds, but it's the difference between:</p>
<ul>
<li><p>❌ "Here are 50 random legal documents that mention your keywords"</p>
</li>
<li><p>✅ "Here's exactly what Section 120B of the IPC says about criminal conspiracy, with relevant case precedents"</p>
</li>
</ul>
<p>It transforms a simple search engine into an intelligent legal assistant that understands context, catches user errors, prevents security threats, and delivers precise, relevant guidance.</p>
<p>Every time a user gets a perfectly tailored legal response, it's because this invisible army of text detectives worked tirelessly behind the scenes to understand not just what was asked, but what was really needed.</p>
<h2 id="heading-what-i-would-do-differently-lessons-from-the-trenches"><strong>What I Would Do Differently: Lessons from the Trenches 🛠️</strong></h2>
<p>While writing this article, I discovered a fascinating gap between what I thought my system was doing and what it actually does. Here are the key improvements I'd make:</p>
<h3 id="heading-conflict-detection">Conflict Detection</h3>
<p>I would implement a validation layer that detects when users mention contradictory legal concepts, like criminal sections in civil law contexts that would either correct the query automatically or ask for clarification.</p>
<p>When conflicts are detected, it would prompt users with something like "I see you mentioned Section 120B (criminal law) and contract law. Did you mean criminal conspiracy or contract violations?"</p>
<pre><code class="lang-markdown">💡 <span class="hljs-strong">**Technical Note:**</span>  
This entire preprocessing pipeline runs before every query to keep responses consistent, secure, and context-aware. It’s invisible to the user — they just get smarter, safer answers — but it’s a huge part of what makes the system reliable.
</code></pre>
<hr />
<h3 id="heading-wrapping-up">Wrapping Up 🎯</h3>
<p>Query preprocessing might seem invisible, but it’s the silent workhorse that turns vague, messy legal questions into something your AI system can actually reason about. Behind every quick, precise answer is a flurry of normalization, detection, intent analysis, and safety checks — all happening in milliseconds.</p>
<p><strong>Key lesson:</strong> the better you prepare and structure a user’s query, the smarter and safer your AI becomes.</p>
<p>But this is just one part of the journey.<br /><strong>Next up:</strong> I’ll share how I measure answer quality and make the whole system faster and cheaper — caching strategies, evaluation datasets, and performance tuning. Stay tuned!</p>
]]></content:encoded></item><item><title><![CDATA[Metadata: The Unsung Hero Behind Smart Document AI 🔎]]></title><description><![CDATA[The Problem: When AI meets Legal Chaos 🤯
Picture this: you're building an AI legal assistant that needs to understand thousands of legal documents. Your users ask questions like "What does Section 120A say about conspiracy?" or "Show me constitution...]]></description><link>https://nityamalhotra.hashnode.dev/metadata-the-unsung-hero-behind-smart-document-ai</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/metadata-the-unsung-hero-behind-smart-document-ai</guid><category><![CDATA[generative ai]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[#DeveloperJourney]]></category><category><![CDATA[genai]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Tue, 30 Sep 2025 14:23:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/9DZsVF-qLaY/upload/66c0a71fa157e60dc4ba082eeb293bd6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-the-problem-when-ai-meets-legal-chaos">The Problem: When AI meets Legal Chaos <strong>🤯</strong></h3>
<p>Picture this: you're building an AI legal assistant that needs to understand thousands of legal documents. Your users ask questions like "What does Section 120A say about conspiracy?" or "Show me constitutional provisions on privacy." Sounds simple, right?</p>
<p>Wrong! Here's what I discovered when I first tried to build this system: throwing raw documents into a vector database and hoping for the best is like asking someone to find a specific book in a library where all the books have been stripped of their covers, titles, and any organizing information. Sure, the content is there, but good luck finding what you need!</p>
<p>Legal documents are particularly tricky because they're not just text – they're highly structured, cross-referenced, and context-dependent. A section from the Indian Penal Code carries very different weight than a similar-sounding passage from a procedure manual. Your AI needs to understand these nuances, or it'll give users dangerously misleading advice.</p>
<p>That's when I realized: metadata isn't just nice-to-have information – it's the intelligence layer that transforms dumb text retrieval into smart legal reasoning.</p>
<h2 id="heading-building-the-metadata-foundation-more-than-just-labels"><strong>Building the Metadata Foundation: More Than Just Labels 🏗️</strong></h2>
<p>So I built a comprehensive metadata extraction system. But this isn't your typical "extract title and author" metadata – this is forensic-level document analysis. Every time a document enters my system, it goes through what I call the "legal autopsy" process.</p>
<p>Here’s the foundation, the ‘extract_legal_metadata’ function that serves as the entry point for this analysis:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">extract_legal_metadata</span>(<span class="hljs-params">text: str, filename: str</span>) -&gt; Dict:</span>
    <span class="hljs-string">"""
    Extract legal metadata with improved section detection and better filtering.
    """</span>
    metadata: Dict = {
        <span class="hljs-string">"source_file"</span>: filename,
        <span class="hljs-string">"document_type"</span>: <span class="hljs-string">"legal_document"</span>, 
        <span class="hljs-string">"jurisdiction"</span>: <span class="hljs-string">"india"</span>,
        <span class="hljs-string">"extracted_acts"</span>: [],
        <span class="hljs-string">"extracted_acts_norm"</span>: [],
        <span class="hljs-string">"extracted_sections"</span>: [],
        <span class="hljs-string">"extracted_sections_norm"</span>: [],
        <span class="hljs-string">"referenced_sections"</span>: [],
        <span class="hljs-string">"referenced_acts"</span>: [],
        <span class="hljs-string">"legal_topics"</span>: [],
        <span class="hljs-string">"legal_topics_norm"</span>: [],
        <span class="hljs-string">"complexity_level"</span>: <span class="hljs-string">"intermediate"</span>,
        <span class="hljs-string">"filename_norm"</span>: _norm_token(filename),
    }
</code></pre>
<p>Notice what's happening here: I'm not just creating a simple key-value store. I'm building a rich semantic profile for each document. The <code>_norm</code> fields? Those are normalized versions for consistent searching – because "IPC" and "Indian Penal Code" should be treated as the same thing when someone's looking for information.</p>
<p>But creating the structure is just the beginning. The real magic happens in the analysis that follows.</p>
<h3 id="heading-chapter-1-the-document-identity-crisis-what-am-i-looking-at">Chapter 1: The Document Identity Crisis - What Am I Looking At?🎭</h3>
<p>The first challenge I faced was document classification. When you're dealing with legal documents, context is everything. A statement about "punishment for theft" means completely different things if it appears in the Indian Penal Code versus a law school textbook versus a Supreme Court judgment.</p>
<p>So I built a document type detector that acts like a digital forensics expert:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">identify_document_type</span>(<span class="hljs-params">filename: str, text: str</span>) -&gt; str:</span>
    <span class="hljs-string">"""Identify the type of legal document"""</span>
    filename_lower = filename.lower()
    text_lower = text.lower()

    <span class="hljs-keyword">if</span> <span class="hljs-string">'constitution'</span> <span class="hljs-keyword">in</span> filename_lower:
        <span class="hljs-keyword">return</span> <span class="hljs-string">'constitution'</span>
    <span class="hljs-keyword">elif</span> <span class="hljs-string">'ipc'</span> <span class="hljs-keyword">in</span> filename_lower <span class="hljs-keyword">or</span> <span class="hljs-string">'penal code'</span> <span class="hljs-keyword">in</span> text_lower:
        <span class="hljs-keyword">return</span> <span class="hljs-string">'criminal_code'</span>
    <span class="hljs-keyword">elif</span> <span class="hljs-string">'crpc'</span> <span class="hljs-keyword">in</span> filename_lower <span class="hljs-keyword">or</span> <span class="hljs-string">'criminal procedure'</span> <span class="hljs-keyword">in</span> text_lower:
        <span class="hljs-keyword">return</span> <span class="hljs-string">'procedure_code'</span>
    <span class="hljs-keyword">elif</span> <span class="hljs-string">'evidence'</span> <span class="hljs-keyword">in</span> filename_lower:
        <span class="hljs-keyword">return</span> <span class="hljs-string">'evidence_act'</span>
    <span class="hljs-keyword">elif</span> <span class="hljs-string">'judgment'</span> <span class="hljs-keyword">in</span> text_lower <span class="hljs-keyword">or</span> <span class="hljs-string">'petitioner'</span> <span class="hljs-keyword">in</span> text_lower:
        <span class="hljs-keyword">return</span> <span class="hljs-string">'case_law'</span>
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-string">'legal_document'</span>
</code></pre>
<p>Why does this matter so much? Because when someone asks "What's the punishment for Section 302?", my system needs to know to look specifically in criminal codes, not in constitutional law or civil procedures. This classification becomes the first filter in my retrieval pipeline – it's like having a smart librarian who knows exactly which section of the library to send you to.</p>
<p>But knowing what type of document you're dealing with is just the beginning. The real detective work starts when you need to understand what's inside.</p>
<h3 id="heading-chapter-2-the-great-section-hunt-finding-legal-dna">Chapter 2: The Great Section Hunt - Finding Legal DNA 🔎</h3>
<p>Now comes the really tricky part. Legal documents are absolutely packed with section references – "Section 120A", "Section 376AB", "Section 153B". But here's the problem I discovered: not every number in a legal document is a section reference!</p>
<p>I learned this the hard way (very hard way) when my early system started flagging page numbers, paragraph numbers, and even dates as "legal sections." Imagine asking about "Section 2023" and getting results about the year instead of actual legal provisions!</p>
<p>So I developed a multi-strategy section detection system that works like a forensic investigator:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Method 1: Find sections with em-dash formatting</span>
<span class="hljs-keyword">for</span> match <span class="hljs-keyword">in</span> SECTION_WITH_EMDASH_RE.finditer(text):
    raw = match.group(<span class="hljs-number">1</span>)
    sec = _norm_section_token(raw)

    <span class="hljs-keyword">if</span> _is_valid_section_token(sec) <span class="hljs-keyword">and</span> sec <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> seen_contained:
        seen_contained.add(sec)
        contained_sections.append(sec)

<span class="hljs-comment"># Method 2: Find sections with titles  </span>
<span class="hljs-keyword">for</span> match <span class="hljs-keyword">in</span> SECTION_WITH_TITLE_RE.finditer(text):
    raw = match.group(<span class="hljs-number">1</span>)
    sec = _norm_section_token(raw)

    <span class="hljs-keyword">if</span> _is_valid_section_token(sec) <span class="hljs-keyword">and</span> sec <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> seen_contained:
        seen_contained.add(sec)
        contained_sections.append(sec)
</code></pre>
<p>The key insight here is that I'm not just doing pattern matching – I'm doing contextual pattern matching. The system looks for sections in their natural habitat: with proper formatting, with titles, in section lists. It's like the difference between finding a phone number written on a napkin versus finding it in a proper contact list.</p>
<p>But even this isn't enough, because legal documents love to mess with your expectations. Which brings me to the next challenge...</p>
<h3 id="heading-chapter-3-the-normalization-challenge-making-chaos-searchable">Chapter 3: The Normalization Challenge - Making Chaos Searchable 🔧</h3>
<p>Here's a problem that nearly drove me crazy: legal section references are incredibly inconsistent. The same section might appear as "Section 120-A", "Sec 120A", "s. 120A", or "120A". When someone searches for "120A", they should find documents containing ANY of these variations.</p>
<p>That's why I built a normalization system:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_norm_section_token</span>(<span class="hljs-params">raw: str</span>) -&gt; str:</span>
    <span class="hljs-string">"""Normalize '153-b'/'153B'/'376AB'/'41' -&gt; '153B','376AB','41' (no hyphens, suffix upper)."""</span>
    tok = re.sub(<span class="hljs-string">r"[\s\-]+"</span>, <span class="hljs-string">""</span>, raw <span class="hljs-keyword">or</span> <span class="hljs-string">""</span>)
    m = re.match(<span class="hljs-string">r"^(\d{1,4})([A-Za-z]{0,3})$"</span>, tok)
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> m:
        <span class="hljs-keyword">return</span> tok.upper()
    num, suf = m.groups()
    <span class="hljs-keyword">return</span> num + (suf.upper() <span class="hljs-keyword">if</span> suf <span class="hljs-keyword">else</span> <span class="hljs-string">""</span>)
</code></pre>
<p>This function is like a universal translator for legal references. It takes the chaos of real-world legal writing and creates clean, searchable tokens.</p>
<h3 id="heading-chapter-4-the-intelligence-layer-where-metadata-becomes-magic">Chapter 4: The Intelligence Layer - Where Metadata Becomes Magic 🎯</h3>
<p>Now comes the moment of truth: putting all this metadata to work. Remember our original problem – users asking "What does the Constitution say about privacy?" and getting random results? Here's how the metadata system solves it.</p>
<p>When a query comes in, my retrieval system follows a sophisticated pipeline:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">retrieve_with_filters</span>(<span class="hljs-params">self, query, filters=None, k=<span class="hljs-number">5</span></span>):</span>
    <span class="hljs-comment"># Stage 1: Get broad candidates from semantic/hybrid search</span>
    <span class="hljs-keyword">if</span> self.has_bm25:
        docs = self.ensemble_retriever.get_relevant_documents(query)
    <span class="hljs-keyword">else</span>:
        docs = self.semantic_retriever.get_relevant_documents(query)

    <span class="hljs-comment"># Stage 2: Apply metadata-based filtering to narrow down results</span>
    <span class="hljs-keyword">if</span> filters:
        docs = self._apply_filters(docs, filters)

    <span class="hljs-comment"># Stage 3: Rerank documents for better relevance</span>
    docs = self._rerank_documents(docs, query)
</code></pre>
<p>Here's what's actually happening: the system first casts a wide semantic net to find potentially relevant content, then uses the metadata to intelligently filter those results. It's like having a research assistant who first gathers a broad collection of potentially relevant materials, then carefully sorts through them using domain expertise.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_apply_filters</span>(<span class="hljs-params">self, docs: List[Document], filters: Dict</span>) -&gt; List[Document]:</span>
    <span class="hljs-string">"""Apply metadata-based filtering"""</span>
    filtered_docs = []

    <span class="hljs-keyword">for</span> doc <span class="hljs-keyword">in</span> docs:
        match_results = []

        <span class="hljs-comment"># document_type (exact match)</span>
        <span class="hljs-keyword">if</span> <span class="hljs-string">'document_type'</span> <span class="hljs-keyword">in</span> filters:
            match_results.append(doc.metadata.get(<span class="hljs-string">'document_type'</span>) == filters[<span class="hljs-string">'document_type'</span>])

        <span class="hljs-comment"># legal_topics (any overlap)</span>
        <span class="hljs-keyword">if</span> <span class="hljs-string">'legal_topics'</span> <span class="hljs-keyword">in</span> filters:
            doc_topics = _to_list(doc.metadata.get(<span class="hljs-string">'legal_topics'</span>, []))
            filter_topics = _to_list(filters[<span class="hljs-string">'legal_topics'</span>])
            has_overlap = any(topic <span class="hljs-keyword">in</span> doc_topics <span class="hljs-keyword">for</span> topic <span class="hljs-keyword">in</span> filter_topics)
            match_results.append(has_overlap)
</code></pre>
<p>The real power comes from the combination of filters. The system can simultaneously filter by document type ("only look in constitutional documents"), by sections ("only pages that reference Article 21"), and by legal topics ("only content about privacy rights"). This multi-dimensional filtering is what transforms generic search results into precise legal research.</p>
<p>This approach ensures that semantically similar content gets retrieved first (so nothing relevant gets missed), but then the metadata acts as a precision filter to ensure legal accuracy.</p>
<hr />
<h2 id="heading-the-transformation-from-chaos-to-intelligence"><strong>The Transformation: From Chaos to Intelligence 🎯</strong></h2>
<p>So what did all this metadata engineering actually accomplish? Let me paint you a before-and-after picture.</p>
<p><strong>Before:</strong> User asks "What does Section 120A say about conspiracy?" System does semantic search, returns three pages about conspiracy theories from random documents, one page about a different Section 120 from civil law, and maybe – if they're lucky – something actually related to Section 120A of the Indian Penal Code.</p>
<p><strong>After:</strong> User asks the same question. System immediately identifies this as a criminal law query, filters to criminal code documents, finds pages that specifically contain or reference Section 120A, and returns precise, contextual information about conspiracy under the Indian Penal Code. Every result comes with clear source attribution and document context.</p>
<p>The difference? Metadata transformed a generic text search into an intelligent legal research assistant.</p>
<h2 id="heading-the-memory-metaphor-why-this-matters-beyond-legal-ai"><strong>The Memory Metaphor: Why This Matters Beyond Legal AI 🧠</strong></h2>
<p>Think about human memory for a moment. You don't just remember facts – you remember context, connections, and categories. When someone mentions "Paris," your brain instantly accesses not just the word, but whether they mean the city or the person, whether you're talking about France or Texas, and what context makes sense.</p>
<p>That's exactly what metadata does for AI systems. It provides the contextual memory that transforms pattern matching into understanding.</p>
<p>In my legal assistant, metadata is the system's memory of what documents are, how they relate to each other, and why certain information matters in certain contexts. Without this memory, you just have a very expensive search engine. With it, you have something that begins to approach intelligence.</p>
<h2 id="heading-the-architecture-lesson-metadata-as-a-first-class-citizen"><strong>The Architecture Lesson: Metadata as a First-Class Citizen 🏗️</strong></h2>
<p>The biggest lesson from building this system? Don't treat metadata as an afterthought. From day one, I designed the entire architecture around rich, contextual metadata. Every component – from document ingestion to query processing to result ranking – was built to leverage and enhance this metadata.</p>
<p>This paid dividends in ways I didn't expect:</p>
<ul>
<li><p><strong>Debugging became trivial</strong>: When something went wrong, the metadata told me exactly what happened and why</p>
</li>
<li><p><strong>Performance optimization was targeted</strong>: I knew exactly which operations were expensive and why</p>
</li>
<li><p><strong>Quality improvements were measurable</strong>: I could track precisely how changes affected different types of queries</p>
</li>
<li><p><strong>Scaling was predictable</strong>: The metadata helped me understand system behavior under load</p>
</li>
</ul>
<h3 id="heading-what-id-do-differently">What I’d Do Differently 🛠️</h3>
<p>If I could go back in time (or send a memo to my past self), I’d change one big thing — <strong>the order of search and filtering.</strong></p>
<p>Right now, my system does the heavy semantic search first and only then applies metadata filters. It works… but it’s a bit like digging through a whole haystack before realizing you could have thrown away 90% of the hay with one quick sift.</p>
<p>Next time, I’d flip it:</p>
<ul>
<li><p><strong>Cheap filters first</strong> — use metadata like document type, act names, or jurisdiction to instantly shrink the search space.</p>
</li>
<li><p><strong>Semantic search second</strong> — run the expensive stuff only on what’s actually relevant.</p>
</li>
</ul>
<p>The payoff? Faster responses, lower cost, cleaner debugging.<br />Interestingly, I only recognized this inefficiency while documenting the system for this article — explaining the design often reveals blind spots you don’t notice while building.</p>
<hr />
<p>And this is just one part of the bigger story. Next, we’ll switch sides — from making documents smart to making <strong>user questions</strong> smart. In the upcoming post, I’ll break down how query preprocessing cleans, normalizes, and enriches raw questions before they ever touch the retrieval engine.</p>
]]></content:encoded></item><item><title><![CDATA[Building a RAG-Powered Legal Document Q&A System with LangChain]]></title><description><![CDATA[The problem that started it all 🤔
Picture this: You're trying to understand a legal document (maybe your lease agreement, or researching a compliance requirement for your startup), and you're drowning in dense, archaic language. You Google your ques...]]></description><link>https://nityamalhotra.hashnode.dev/building-a-rag-powered-legal-document-qanda-system-with-langchain</link><guid isPermaLink="true">https://nityamalhotra.hashnode.dev/building-a-rag-powered-legal-document-qanda-system-with-langchain</guid><category><![CDATA[RAG ]]></category><category><![CDATA[langchain]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[llm]]></category><category><![CDATA[LLM's ]]></category><category><![CDATA[#DeveloperJourney]]></category><dc:creator><![CDATA[Nitya]]></dc:creator><pubDate>Tue, 30 Sep 2025 09:39:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/OAsF0QMRWlA/upload/271577efb46082377f906de3e5d16657.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-problem-that-started-it-all">The problem that started it all <strong>🤔</strong></h2>
<p>Picture this: You're trying to understand a legal document (maybe your lease agreement, or researching a compliance requirement for your startup), and you're drowning in dense, archaic language. You Google your question and get either overly simplified blog posts or incomprehensible legal jargon. Sound familiar?</p>
<p>I thought: <em>There has to be a better way to make legal information accessible.</em><br />That thought led me to build an <strong>AI-powered legal document Q&amp;A assistant</strong> using <strong>Retrieval-Augmented Generation (RAG)</strong> and <strong>LangChain</strong>.</p>
<p>The result? A system that can answer questions like:</p>
<blockquote>
<p>“I once got stopped in my hometown for driving my white car after I’d painted the roof black. What’s the penalty for that?”</p>
</blockquote>
<p>It finds the right legal text and explains it in plain language — without me manually digging through 200-page PDFs.</p>
<h2 id="heading-why-rag-and-what-even-is-it"><strong>Why RAG? (And What Even Is It?) 🧠</strong></h2>
<p>When I first heard about <strong>RAG (Retrieval-Augmented Generation)</strong>, I was skeptical. Another AI buzzword? But the more I learned, the more it solved exactly the pain I had.</p>
<p><strong>The Challenge</strong>: Large Language Models (LLMs) like GPT-4 are incredibly smart, but they have two major limitations:</p>
<ul>
<li><p><strong>Knowledge cutoff</strong>: They don't know about documents created after their training</p>
</li>
<li><p><strong>Hallucination</strong>: They sometimes make up facts that sound convincing but are wrong</p>
</li>
</ul>
<p><strong>The RAG Solution:</strong> Instead of asking the LLM to generate answers from memory, we:</p>
<ol>
<li><p><strong>Retrieve</strong> relevant information from our own documents</p>
</li>
<li><p><strong>Augment</strong> the LLM prompt with this context</p>
</li>
<li><p><strong>Generate</strong> answers based on the retrieved facts</p>
</li>
</ol>
<p>Think of it like giving the AI a legal library to reference before answering questions, rather than relying purely on what it memorized during training.</p>
<blockquote>
<p><strong>💡 Pro Tip</strong>: RAG is perfect when you need AI to work with domain-specific, up-to-date, or proprietary information — exactly what legal documents require!</p>
</blockquote>
<p><strong>Architecture: Big Picture🏗️</strong></p>
<p>Before diving into code, here’s the 30,000-foot view. I split the system into two phases:</p>
<ol>
<li><p><strong>Ingestion (prepares your knowledge base once).</strong></p>
</li>
<li><p><strong>Query-time (answers user questions using that base).</strong></p>
</li>
</ol>
<pre><code class="lang-mermaid">flowchart TB
  subgraph Ingestion
    A["Legal PDFs"] --&gt; B["Document processing"]
    B --&gt; C["Vector DB"]
  end

  subgraph QueryTime
    D["User query"] --&gt; E["Query processing"]
    E --&gt; F["Hybrid retrieval"]
    C --&gt; F
    F --&gt; G["LLM generation / RAG QA chain"]
    G --&gt; H["Answer to user"]
  end
</code></pre>
<ul>
<li><p><strong>Ingestion</strong>: PDFs are chunked, converted into embeddings, and stored in a vector database.</p>
</li>
<li><p><strong>Query-time</strong>: A user question is cleaned up, used to fetch relevant chunks (hybrid retrieval = semantic + keyword), and passed to the LLM to generate an answer.</p>
</li>
</ul>
<h2 id="heading-implementation-highlights">Implementation Highlights 🛠️</h2>
<p>I kept my first build simple, then added improvements once the basics worked.</p>
<h3 id="heading-document-processing-amp-chunking">Document Processing &amp; Chunking</h3>
<p>Legal PDFs can be huge. I broke them into ~1000-character chunks with 200-character overlap so context isn’t lost at section breaks.</p>
<pre><code class="lang-python">text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=<span class="hljs-number">1000</span>,
    chunk_overlap=<span class="hljs-number">200</span>,
)
</code></pre>
<p><strong>Why these numbers?</strong> After experimentation, I found 1000 characters gave enough context without overwhelming the LLM, while 200-character overlap ensured important information wasn't lost at chunk boundaries.</p>
<blockquote>
<p><strong>💡 Pro Tip</strong>: <strong>Metadata is crucial</strong>: Each chunk in my RAG includes: Source document name, extracted sections, extracted acts and document type.</p>
</blockquote>
<h3 id="heading-embeddings-with-chromadb">Embeddings with ChromaDB</h3>
<p>Here's where things get interesting. <strong>Embeddings</strong> are numerical representations of text that capture semantic meaning. Similar concepts have similar embeddings.</p>
<p><strong>Why ChromaDB?</strong></p>
<ul>
<li><p>✅ <strong>Local-first</strong>: No vendor lock-in, runs on your machine</p>
</li>
<li><p>✅ <strong>Persistent storage</strong>: Survives restarts</p>
</li>
<li><p>✅ <strong>Metadata filtering</strong>: Query by document type, sections, etc.</p>
</li>
<li><p>✅ <strong>Free</strong>: No per-query costs like hosted solutions (Perfect for development)</p>
</li>
</ul>
<p><strong>Smart caching</strong>: I implemented document hash-based cache validation so embeddings are only regenerated when documents actually change — saving both time and money (More about this in upcoming articles)</p>
<h3 id="heading-hybrid-retrieval-magic">Hybrid Retrieval Magic 🔍</h3>
<p>Here's where my system gets clever. Instead of relying solely on semantic search, I implemented <strong>hybrid retrieval</strong> using LangChain's Ensemble Retriever.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Combine keyword and semantic search</span>
self.ensemble_retriever = EnsembleRetriever(
    retrievers=[self.bm25_retriever, self.semantic_retriever],
    weights=[<span class="hljs-number">0.4</span>, <span class="hljs-number">0.6</span>]  <span class="hljs-comment"># 40% keyword, 60% semantic</span>
)
</code></pre>
<p><strong>Why hybrid works better:</strong></p>
<ul>
<li><p><strong>BM25 (keyword search)</strong>: Perfect for exact matches like "Section 302"</p>
</li>
<li><p><strong>Semantic search</strong>: Excellent for conceptual queries like "punishment for stealing"</p>
</li>
<li><p><strong>Combined power</strong>: Gets the best of both worlds</p>
</li>
</ul>
<h3 id="heading-smart-query-preprocessing-my-secret-sauce">Smart Query Preprocessing (My Secret Sauce)</h3>
<p>Before sending user questions to the LLM, I run them through a <em>tiny but mighty</em> preprocessing layer. Think of it as a brainy assistant that quickly figures out what the query is <em>really asking</em> — extracting key legal details (like section numbers, acts, and intent) and shaping smarter filters.</p>
<p>Why bother? Because legal queries can be tricky — “Section 302 punishment” shouldn’t lead you to corporate compliance docs! My preprocessor helps the system keep the wide net of semantic search but adds a laser-focused legal filter before answering. <em>(I am writing a deep dive on this if you’re curious — but that’s a story for another day 👀).</em></p>
<hr />
<h2 id="heading-lessons-learned">Lessons Learned 🎯</h2>
<ul>
<li><p>🧠 <strong>RAG is the bridge</strong> — it turns a general LLM into a domain expert on your data.</p>
</li>
<li><p>🔍 <strong>Metadata matters</strong> — rich chunk metadata = better filters and citations.</p>
</li>
<li><p>🚀 <strong>Hybrid &gt; single</strong> — combining retrieval methods improves accuracy.</p>
</li>
<li><p>🪄 <strong>Keep preprocessing simple at first</strong> — fancy query rewriting is nice later, but a working baseline RAG flow is better to ship early.</p>
</li>
<li><p>⚖️ <strong>Legal is just one domain</strong> — the same pattern fits finance, healthcare, compliance docs, or any specialized text.</p>
</li>
</ul>
<hr />
<h2 id="heading-wrapping-up-from-backend-to-ai-amp-whats-next"><strong>Wrapping Up: From Backend to AI</strong> &amp; What’s Next</h2>
<p>Building this system taught me that working with LLMs isn’t so different from traditional backend engineering — you’re still designing data flows, optimizing performance, and building reliable components. The difference is that the “database” is now a vector store, and your “API responses” are LLM generations.</p>
<p><strong>Key takeaways:</strong></p>
<ul>
<li><p>🧠 RAG bridges the gap between general AI and domain knowledge.</p>
</li>
<li><p>🔍 Metadata and smart chunking pay off later when you want precise answers.</p>
</li>
<li><p>🚀 Hybrid retrieval (semantic + keyword) can dramatically improve relevance.</p>
</li>
<li><p>⚡ Start simple, then refine — build a working loop first before optimizing.</p>
</li>
</ul>
<p>This post was a high-level look at my AI Legal Assistant project. In upcoming articles, I’ll dig deeper into specific parts — like evaluation datasets, caching strategies, and performance tuning.</p>
<hr />
<p><em>What domain would you tackle with RAG? What challenges do you think you'd face? Drop your thoughts in the comments! 👇</em></p>
]]></content:encoded></item></channel></rss>