<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Isaac Natarajan</title>
    <description>The latest articles on DEV Community by Isaac Natarajan (@isaacnatarajan).</description>
    <link>https://dev.to/isaacnatarajan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4002846%2F166a1ec3-97b5-4c53-9945-9377b06fd7fe.jpeg</url>
      <title>DEV Community: Isaac Natarajan</title>
      <link>https://dev.to/isaacnatarajan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isaacnatarajan"/>
    <language>en</language>
    <item>
      <title>Building my-assistant: teaching an AI agent to find my own files</title>
      <dc:creator>Isaac Natarajan</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:39:00 +0000</pubDate>
      <link>https://dev.to/isaacnatarajan/building-my-assistant-teaching-an-ai-agent-to-find-my-own-files-i3g</link>
      <guid>https://dev.to/isaacnatarajan/building-my-assistant-teaching-an-ai-agent-to-find-my-own-files-i3g</guid>
      <description>&lt;p&gt;&lt;strong&gt;Note: this post covers V1 of the project. It's a real, working assistant I use daily — not a finished product. There's a V2 in the works, and I'd genuinely love your ideas on it (more on that at the end).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F63aj8jbk9of88r8ymwh1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F63aj8jbk9of88r8ymwh1.png" alt=" " width="800" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I built this&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I started learning agentic AI the way most people probably do — watching a course, working through LangChain and LangGraph fundamentals one concept at a time: state, nodes, edges, tools, memory, human-in-the-loop, guardrails. It's a lot of scaffolding before you get to build anything real, and at some point the tutorial-project itch wears off. I didn't want to build another "spec-to-API agent" demo that I'd never open again. I wanted something I'd actually use.&lt;/p&gt;

&lt;p&gt;So I picked a problem I genuinely have: too many files, scattered across Downloads, Desktop, and Documents, and no memory of where anything is. The result is my-assistant — an always-on-top desktop widget where I can just ask, in plain English, "what did I write about TaskFlow's approval flow?" or "open my timesheet from last week," and it finds it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it actually does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under the hood, it's one AI agent — not a swarm of them, more on that below — built with LangChain and LangGraph, backed by a local vector database (Qdrant) for semantic search, and a Groq-hosted model for the actual reasoning. It has six tools: search by content, search by filename, open a file or folder, count indexed files, list indexed folders, and index new files. A Flet-based UI wraps all of it into a small, always-on-top chat widget that sits on my desktop.&lt;/p&gt;

&lt;p&gt;The part I'm most proud of isn't the search — it's that the index stays alive on its own. A background file-system watcher notices when I download something new, edit an existing document, or delete a file, and updates the index automatically, without me ever running a manual command. Ask it "index my new PDFs" and it'll do that too, or just let it work silently in the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One agent, not many — and why that took me a while to actually understand&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Early on, I assumed a "long process with lots of steps" — scan folders, extract text, chunk it, embed it, store it — meant I needed multiple agents working together. It doesn't, and figuring out why it doesn't was one of the more useful lessons of this whole project.&lt;/p&gt;

&lt;p&gt;The thing that actually determines whether you need an agent isn't how many steps a process has — it's whether any step requires judgment on ambiguous input. Extracting text from a PDF has exactly one correct way to do it, every time. So does chunking, embedding, and writing to a vector store. None of that needs an LLM's reasoning — it needs a deterministic pipeline. The only place genuine ambiguity shows up is right at the front: interpreting what I actually meant when I typed something into the chat box. That's the entire agentic surface area. Everything after it is plumbing.&lt;/p&gt;

&lt;p&gt;That reframing changed how I think about when multi-agent design is actually worth it — not "this has many steps," but "does some sub-task need a genuinely different kind of judgment or persona than the rest." More on where that might actually apply in V2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The debugging stories that taught me the most&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few things went wrong along the way that ended up teaching me more than the parts that worked first try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The index that quietly ate my own virtual environments.&lt;/strong&gt; My exclusion logic checked for folders literally named venv or .venv — until I found one named venv_rag, and hundreds of site-packages files from unrelated projects showed up in my search results. The fix wasn't adding more exact name checks; it was detecting virtual environments structurally, by the presence of pyvenv.cfg, so it doesn't matter what anyone names them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Storage folder is already accessed by another instance."&lt;/strong&gt; Once I added a background watcher running independently of the chat agent, both started occasionally trying to open their own separate connections to the same local Qdrant database at the same moment — something that's fine for a single sequential agent, but breaks the instant you have two independent threads touching the same embedded database. The fix was a shared singleton connection instead of a fresh one per call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent that wouldn't stop flailing.&lt;/strong&gt; I asked about a table buried inside a Word document, and the agent, unsatisfied with its first search, started calling unrelated tools — trying to re-index files, trying to physically open the document in Word — cycling between them instead of just telling me it couldn't find something. The actual root cause, once I dug in with a raw database query, was almost funny: the content was there all along. It just wasn't ranking in the top 3 search results for that particular phrasing. The real fixes were tightening the system prompt so the agent stops and tells me instead of flailing, and widening how many results it pulls per search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A spreadsheet answer that quietly wasn't stale, but the reasoning behind it was invented.&lt;/strong&gt; Asking about a folder's contents, the agent gave me the right file count, but with a made-up explanation for why it couldn't show subfolders — a limitation that didn't actually exist. It's a good reminder that a technically correct answer can still come with confidently wrong reasoning attached, and it's worth checking both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows Explorer creating a "deleted" file that was never indexed.&lt;/strong&gt; Creating a new blank text file somehow triggered a "removed from index" message before the file even had content. Turned out Explorer's file-creation flow fires as a rename event under the hood, and my delete-handler was printing success messages unconditionally, regardless of whether anything was actually removed. A small bug, but the kind that erodes trust in a tool's logs if you don't catch it.&lt;/p&gt;

&lt;p&gt;None of these were exotic problems. They were the ordinary friction of building something real instead of following a tutorial — and that's exactly why they were worth having.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next (and where I'd like your ideas)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is V1. It works, and I use it daily, but it's not the end state. A few directions I'm considering for V2, specifically where I think multiple specialized agents would actually earn their keep rather than just be complexity for its own sake:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;writing agent&lt;/strong&gt; that can draft summaries or emails from documents the file agent retrieves — a genuinely different skill and voice than "find and open files."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;vision agent&lt;/strong&gt; for screenshots and scanned documents, if reasoning about images turns out to need real judgment rather than a single API call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;proactive mode&lt;/strong&gt; that notices clutter or duplicate files and suggests cleanup, instead of only reacting to what I ask.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Packaging it as a real standalone Windows app, so it doesn't need a terminal window open in the background.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built something like this, or have a feature you'd genuinely want out of a personal file assistant, I'd like to hear it — drop a comment or reach out. Still very much a work in progress, and that's kind of the point.&lt;/p&gt;

&lt;p&gt;GitHub :- &lt;a href="https://github.com/IsaacNatarajan/My-Assistant/tree/main" rel="noopener noreferrer"&gt;https://github.com/IsaacNatarajan/My-Assistant/tree/main&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>agentskills</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Built an AI That Interviews You Like a Real Mentor — Here's What I Learned About Multi-Agent Systems</title>
      <dc:creator>Isaac Natarajan</dc:creator>
      <pubDate>Fri, 03 Jul 2026 18:28:27 +0000</pubDate>
      <link>https://dev.to/isaacnatarajan/i-built-an-ai-that-interviews-you-like-a-real-mentor-heres-what-i-learned-about-multi-agent-5e93</link>
      <guid>https://dev.to/isaacnatarajan/i-built-an-ai-that-interviews-you-like-a-real-mentor-heres-what-i-learned-about-multi-agent-5e93</guid>
      <description>&lt;p&gt;A few weeks ago I set out to learn LangGraph. I ended up building something I actually use.&lt;/p&gt;

&lt;p&gt;Here's the origin story, the architecture, and the mistakes that taught me the most.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0o4kiszbftfr6ntd3aav.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0o4kiszbftfr6ntd3aav.png" alt=" " width="800" height="371"&gt;&lt;/a&gt; &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1acjqa4yg38i65t8hrih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1acjqa4yg38i65t8hrih.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem With Interview Prep Tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most interview prep apps are the same thing wearing different skins: a list of questions, a text box, maybe a model answer if you're lucky. They don't react to you. They don't know if you're stuck, confused, or just had a brain fart and typed something completely unrelated. They grade you like a quiz, not coach you like a mentor.&lt;/p&gt;

&lt;p&gt;I wanted to build something different — a system that feels less like a form and more like sitting across from someone who actually wants to see you improve.&lt;/p&gt;

&lt;p&gt;And since I'd already worked with LangChain and LangSmith, this felt like the perfect excuse to finally learn LangGraph properly, by building something with real stakes: multiple agents, real conditional logic, and a genuinely useful end product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting With the Wrong Architecture (On Purpose, Sort Of)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first instinct was the "trendy" AI-app checklist: RAG, a vector database, the works. I was building this in the middle of a big shift in the ecosystem too — mid-project, Groq announced they were deprecating the exact models I'd been using (llama-3.3-70b-versatile and llama-3.1-8b-instant), pushing everyone toward their newer gpt-oss models. A good reminder that in this space, the ground moves under you constantly, and your architecture needs to be flexible enough to swap a model string without a rewrite.&lt;/p&gt;

&lt;p&gt;But early on, a simpler realization changed the whole design: this system doesn't need a knowledge base. The questions aren't retrieved from documents — they're generated live, tailored to a role, an experience level, and an optional job description. Qdrant would have added infrastructure that solved a problem I didn't have.&lt;/p&gt;

&lt;p&gt;So I cut it. MongoDB became the single source of truth for everything — sessions, users, performance history. One database, one mental model, zero unnecessary complexity. Sometimes the best architectural decision is the thing you don't build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Part That Actually Made It "Agentic"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the distinction that took me the longest to internalize: using multiple LLM calls doesn't make a system agentic. Deciding which call to make, dynamically, based on context — that's what does.&lt;/p&gt;

&lt;p&gt;My first version had a rigid pipeline: user answers → evaluator scores it → maybe a hint → done. It worked, but it was fragile. The moment a user typed something like "can we have more questions?" instead of an actual answer, the system would dutifully score it as a wrong answer. Because as far as the code was concerned, every input was "an answer."&lt;/p&gt;

&lt;p&gt;That was the wake-up call. I built an Intent Classifier Agent that runs in front of everything else, reading the user's message and routing it to the right specialist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Genuinely answering the question? → Evaluator Agent&lt;/li&gt;
&lt;li&gt;Stuck and want help? → Hint Generator&lt;/li&gt;
&lt;li&gt;Don't understand the question itself? → Question Clarifier (a new agent I hadn't planned for)&lt;/li&gt;
&lt;li&gt;Want more detail on feedback you already got? → Answer Elaborator (another agent born from user testing, not the original design)&lt;/li&gt;
&lt;li&gt;Talking about the session, not the question? → Off-Topic Handler&lt;/li&gt;
&lt;li&gt;Want to skip? → Skip logic, no scoring, no judgment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nine agents now, each with one job, coordinated through LangGraph's conditional edges instead of a fixed sequence. That's the difference between a chatbot with extra steps and an actual multi-agent system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Bugs That Taught Me the Most&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few humbling moments, because no build log is honest without them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Python scoping bug that looked like a networking issue.&lt;/strong&gt; I had a function reassign a variable (state = hint_generator_agent(state)) inside what I thought was a safe scope. Python quietly decided the whole variable was local to the function from that point on — including before the reassignment — and threw an UnboundLocalError that manifested downstream as a browser CORS error. I spent twenty minutes checking my CORS config before realizing the backend had crashed entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The evaluator that graded a session it never saw.&lt;/strong&gt; Reload a chat mid-session, and the system would ask the exact same question the user had just answered — because the "current question" was only updated when explicitly requested, not the moment an answer came in. Small state-management oversight, big trust-breaking bug if left in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tone problem.&lt;/strong&gt; My evaluator originally wrote feedback like a report: "The candidate demonstrated..." Clinical, third-person, cold. The fix wasn't more intelligence — it was a prompt rewrite asking the model to speak in first person, use the user's name occasionally, and react to what they specifically said rather than generic praise. One prompt change transformed the entire feel of the product from "quiz" to "conversation."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "Agentic" Actually Buys You&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By the end, the system could handle things I never explicitly coded for. Say "I don't get it" and it rephrases the question in its own words — as the same interviewer, not a third party summarizing someone else's question. Ask "can you elaborate?" after getting feedback, and it digs deeper into that specific model answer, not a generic explanation.&lt;/p&gt;

&lt;p&gt;None of that was hardcoded as a feature. It emerged from giving the system the ability to classify intent and route accordingly. That's the part of agentic design that's easy to describe in a sentence and genuinely hard to appreciate until you've built the alternative first and watched it fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker's wired up so anyone can clone the repo, drop in an API key, and run the whole stack — frontend, backend, and MongoDB — with one command. Langfuse traces every agent call, which turned out to be invaluable for actually seeing the routing decisions happen in real time, not just trust that they were happening.&lt;/p&gt;

&lt;p&gt;From here: a proper deployment, resume-based personalization, and maybe a voice mode, because reading and typing answers isn't quite the same pressure as saying them out loud.&lt;/p&gt;

&lt;p&gt;If you're thinking about learning LangGraph, my honest advice: don't build a toy example. Build something with a real, opinionated use case, let real usage expose the gaps in your first design, and be ready to throw away your first architecture when a simpler one reveals itself.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Check out the full project, architecture, and setup instructions on &lt;a href="https://github.com/IsaacNatarajan/AI-Interview-Simulator/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and feedback welcome.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Built a Mental Health Journal Using MCP, Claude Desktop and NVIDIA NIM — Here's How</title>
      <dc:creator>Isaac Natarajan</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:24:57 +0000</pubDate>
      <link>https://dev.to/isaacnatarajan/i-built-a-mental-health-journal-using-mcp-claude-desktop-and-nvidia-nim-heres-how-2lnj</link>
      <guid>https://dev.to/isaacnatarajan/i-built-a-mental-health-journal-using-mcp-claude-desktop-and-nvidia-nim-heres-how-2lnj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuejgvens7814110sz4ka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuejgvens7814110sz4ka.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've been exploring Model Context Protocol (MCP) recently and wanted to build something that was actually useful in my day-to-day life — not just another todo app or weather tool.&lt;/p&gt;

&lt;p&gt;Mental health journaling is something I've always wanted to be consistent with, but the friction of opening an app, typing out my thoughts in a structured way, and then getting no real feedback always stopped me.&lt;/p&gt;

&lt;p&gt;So I built a Mental Health Journal MCP — where I just talk to Claude naturally about my day and it handles everything else automatically. Logging, detecting cognitive distortions, tracking mood trends, suggesting coping strategies — all from a single conversation.&lt;br&gt;
Here's the full story of how I built it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is MCP?&lt;/strong&gt;&lt;br&gt;
Before diving in, let me quickly explain Model Context Protocol (MCP) for those who haven't heard of it.&lt;/p&gt;

&lt;p&gt;MCP is an open protocol by Anthropic that lets you connect AI models like Claude to external tools and data sources. Think of it like giving Claude the ability to call functions — read from a database, call an API, write files — anything you want.&lt;/p&gt;

&lt;p&gt;You build an MCP server that exposes a set of tools, and an MCP client (like Claude Desktop) connects to it and uses those tools automatically during conversation.&lt;/p&gt;

&lt;p&gt;It's a game changer for building AI-powered personal tools.&lt;/p&gt;

&lt;p&gt;The Problem I Wanted to Solve&lt;br&gt;
Traditional journaling apps require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opening an app&lt;/li&gt;
&lt;li&gt;Filling structured forms (date, mood, text)&lt;/li&gt;
&lt;li&gt;Getting no intelligent feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to just say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I had a really stressful day today, kept worrying about my project deadlines and felt like everything was falling apart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And have the system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log the entry automatically&lt;/li&gt;
&lt;li&gt;Detect thinking patterns (cognitive distortions)&lt;/li&gt;
&lt;li&gt;Suggest coping strategies&lt;/li&gt;
&lt;li&gt;Track my mood over time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's exactly what this project does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Desktop (MCP Client)
        ↓
MCP Server (Python + FastMCP)
        ↓
NVIDIA NIM — meta/llama-4-maverick-17b-128e-instruct (AI Judge)
        ↓
SQLite (Local Storage)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decision here is the dual-model pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; handles the conversational layer — empathy, responses, deciding which tool to call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NVIDIA NIM (Llama 4 Maverick)&lt;/strong&gt; acts as the AI judge — focused tasks like detecting cognitive distortions and generating reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps each model doing what it's best at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools I Built&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;log_journal_entry&lt;/td&gt;
&lt;td&gt;Logs date, text, and mood score to SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;detect_cognitive_distortions&lt;/td&gt;
&lt;td&gt;Uses Llama 4 Maverick to analyze thinking patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analyze_mood_trends&lt;/td&gt;
&lt;td&gt;Queries database and calculates mood statistics over a date range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get_stress_triggers&lt;/td&gt;
&lt;td&gt;Identifies low mood entries and uses AI to detect patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;suggest_coping_strategy&lt;/td&gt;
&lt;td&gt;Generates personalized coping advice using AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;generate_weekly_report&lt;/td&gt;
&lt;td&gt;Creates a full weekly mental health summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set_intention&lt;/td&gt;
&lt;td&gt;Sets a daily intention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;check_intention_reflection&lt;/td&gt;
&lt;td&gt;Reflects on how well the intention was followed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Setting Up the Project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir mental-health-journal-mcp
cd mental-health-journal-mcp
python -m venv venv
venv\Scripts\activate
pip install uv
uv pip install mcp httpx python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Database Setup (db.py)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import sqlite3
import os

DB_PATH = os.path.join(os.path.dirname(__file__), "data", "journal.db")

def get_connection():
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    conn = get_connection()
    cursor = conn.cursor()

    cursor.executescript("""
        CREATE TABLE IF NOT EXISTS journal_entries (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            date TEXT NOT NULL,
            text TEXT NOT NULL,
            mood_score INTEGER NOT NULL,
            distortions TEXT,
            triggers TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );

        CREATE TABLE IF NOT EXISTS intentions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            date TEXT NOT NULL,
            intention TEXT NOT NULL,
            reflection TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
    """)

    conn.commit()
    conn.close()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Building the MCP Server&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server Skeleton (server.py)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from mcp.server.fastmcp import FastMCP
from db import init_db, get_connection
from dotenv import load_dotenv
import httpx
import os

load_dotenv()
init_db()

mcp = FastMCP("Mental Health Journal")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logging Journal Entries&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@mcp.tool()
def log_journal_entry(date: str, text: str, mood_score: int) -&amp;gt; str:
    """Log a journal entry with date, text and mood score (1-10)"""

    if not 1 &amp;lt;= mood_score &amp;lt;= 10:
        return "Mood score must be between 1 and 10"

    conn = get_connection()
    cursor = conn.cursor()

    cursor.execute("""
        INSERT INTO journal_entries (date, text, mood_score)
        VALUES (?, ?, ?)
    """, (date, text, mood_score))

    conn.commit()
    conn.close()

    return f"Journal entry logged for {date} with mood score {mood_score}/10"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Detecting Cognitive Distortions (The AI Judge)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where it gets interesting. I use NVIDIA NIM's Llama 4 Maverick as a focused AI judge to analyze journal text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@mcp.tool()
def detect_cognitive_distortions(entry_text: str) -&amp;gt; str:
    """Analyze journal entry text and detect cognitive distortions using AI"""

    api_key = os.getenv("NVIDIA_API_KEY")

    prompt = f"""You are a cognitive behavioral therapy expert. Analyze this journal entry and detect any cognitive distortions present.

Journal entry: {entry_text}

List the cognitive distortions found (e.g. catastrophizing, all-or-nothing thinking, mind reading, overgeneralization etc.) and briefly explain why. Be concise and empathetic."""

    response = httpx.post(
        "https://integrate.api.nvidia.com/v1/chat/completions",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "model": "meta/llama-4-maverick-17b-128e-instruct",
            "messages": [{"role": "user", "content": prompt}],
            "max_tokens": 300
        }
    )

    return response.json()["choices"][0]["message"]["content"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a real output I got:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I've identified two cognitive distortions: Catastrophizing — expecting the worst-case scenario without considering other outcomes. Fortune Telling — predicting future failure without concrete evidence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's genuinely useful feedback from a journal entry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Making it Feel Natural&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The real magic happens with a system prompt in Claude Desktop. I created a Project and added these instructions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are a compassionate mental health journal assistant.&lt;br&gt;
When the user shares how they are feeling or talks about their day:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Always automatically log a journal entry using the log_journal_entry tool with today's date&lt;/li&gt;
&lt;li&gt;If the user doesn't mention a mood score, estimate it from their tone (1-10)&lt;/li&gt;
&lt;li&gt;After logging, automatically run detect_cognitive_distortions on their entry&lt;/li&gt;
&lt;li&gt;Respond empathetically and suggest coping strategies if mood score is 4 or below
When the user asks about their mood, week, patterns or triggers:&lt;/li&gt;
&lt;li&gt;Use analyze_mood_trends, get_stress_triggers or generate_weekly_report automatically
Always be warm, non-judgmental and supportive.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I just talk naturally and everything happens automatically. No commands, no forms, no friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connecting to Claude Desktop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add this to your claude_desktop_config.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "mcpServers": {
    "mental-health-journal": {
      "command": "C:\\path\\to\\venv\\Scripts\\python.exe",
      "args": ["C:\\path\\to\\server.py"]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows the config file is located at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\&amp;lt;username&amp;gt;\AppData\Local\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Claude Desktop and your tools show up automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Learned&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. MCP is incredibly powerful for personal tools&lt;/strong&gt; — the ability to give Claude access to your own data and functions opens up so many possibilities.&lt;br&gt;
&lt;strong&gt;2. The dual-model pattern works well&lt;/strong&gt; — using a lighter, focused model (Llama 4 Maverick) for specific analysis tasks keeps things efficient and consistent.&lt;br&gt;
&lt;strong&gt;3. System prompts are the UX laye&lt;/strong&gt;r — the difference between a tool that feels robotic and one that feels natural is entirely in how you prompt the client model.&lt;br&gt;
&lt;strong&gt;4. SQLite is underrated&lt;/strong&gt; — for personal local tools, SQLite is perfect. Zero setup, fast, private.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The full source code is available on GitHub: &lt;a href="https://github.com/IsaacNatarajan/Mental-Health-Journal/" rel="noopener noreferrer"&gt;https://github.com/IsaacNatarajan/Mental-Health-Journal/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're exploring MCP and want to build something practical, I highly recommend starting with a personal tool like this. The feedback loop of actually using what you build makes learning so much faster.&lt;/p&gt;

&lt;p&gt;Have questions or want to share what you're building with MCP? Drop a comment below! 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>llm</category>
      <category>claude</category>
    </item>
    <item>
      <title>Building an Advanced RAG System with Multiple Chunking Strategies — A Practical Guide</title>
      <dc:creator>Isaac Natarajan</dc:creator>
      <pubDate>Thu, 25 Jun 2026 19:32:15 +0000</pubDate>
      <link>https://dev.to/isaacnatarajan/building-an-advanced-rag-system-with-multiple-chunking-strategies-a-practical-guide-36hj</link>
      <guid>https://dev.to/isaacnatarajan/building-an-advanced-rag-system-with-multiple-chunking-strategies-a-practical-guide-36hj</guid>
      <description>&lt;p&gt;I built an Advanced RAG system that compares 4 chunking strategies (fixed-size, recursive, semantic, hierarchical) on Apple's 10-K filings using NVIDIA NIM models, Qdrant, and custom evaluation metrics. Semantic chunking won with an overall score of 0.86. Here's everything I learned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo7zsyctwmu9otpwffof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo7zsyctwmu9otpwffof.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;br&gt;
Retrieval-Augmented Generation (RAG) is one of the most practical applications of LLMs today. Instead of relying on a model's training data, RAG retrieves relevant information from your own documents and uses it to generate accurate, grounded answers.&lt;/p&gt;

&lt;p&gt;But here's what most RAG tutorials skip: how you chunk your documents matters enormously. The same pipeline with different chunking strategies can produce wildly different results. I wanted to test this properly, so I built a system that runs 4 chunking strategies side by side on the same corpus and evaluates them with real metrics.&lt;/p&gt;

&lt;p&gt;In this post I'll walk through everything — data ingestion, chunking, vector storage, retrieval, generation, evaluation, and a Streamlit chatbot UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Tool / Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Embedding Model&lt;/td&gt;
&lt;td&gt;NVIDIA llama-nemotron-embed-1b-v2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;NVIDIA llama-3.3-nemotron-super-49b-v1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector Database&lt;/td&gt;
&lt;td&gt;Qdrant (local via Docker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline&lt;/td&gt;
&lt;td&gt;LangChain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tracing&lt;/td&gt;
&lt;td&gt;LangSmith&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation&lt;/td&gt;
&lt;td&gt;Custom LLM-as-judge metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Streamlit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All NVIDIA models are accessed via &lt;a href="https://integrate.api.nvidia.com/v1" rel="noopener noreferrer"&gt;https://integrate.api.nvidia.com/v1&lt;/a&gt; which is OpenAI-compatible, making integration straightforward.&lt;/p&gt;

&lt;p&gt;One important quirk with llama-3.3-nemotron-super-49b-v1.5 — it has a thinking mode that needs to be explicitly disabled, and you need high max_tokens (8192+) otherwise the model spends all its tokens on internal reasoning and returns None as content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response = client.chat.completions.create(
    model=LLM_MODEL,
    messages=[...],
    max_tokens=8192,
    extra_body={"chat_template_kwargs": {"thinking": False}}
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, the embedding model is asymmetric and requires an input_type parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# For document chunks
client.embeddings.create(model=EMBED_MODEL, input=text, extra_body={"input_type": "passage"})

# For queries
client.embeddings.create(model=EMBED_MODEL, input=query, extra_body={"input_type": "query"})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Data Ingestion&lt;/strong&gt;&lt;br&gt;
I used Apple's 10-K annual reports for 2022 and 2023, downloaded directly from Apple's investor relations page as PDFs. Financial documents are ideal for this kind of project because they have mixed content — dense paragraphs, tables, numbered sections, and boilerplate — which makes chunking strategy comparison genuinely meaningful.&lt;/p&gt;

&lt;p&gt;Extraction with pdfplumber:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import pdfplumber

def load_pdfs():
    documents = []
    for filename in os.listdir("data/pdfs"):
        if filename.endswith(".pdf"):
            with pdfplumber.open(f"data/pdfs/{filename}") as pdf:
                full_text = ""
                for page in pdf.pages:
                    text = page.extract_text()
                    if text:
                        full_text += text + "\n"
            documents.append({"filename": filename, "text": clean_text(full_text)})
    return documents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After cleaning, I ended up with ~221k characters from the 2022 report and ~207k from 2023.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 4 Chunking Strategies&lt;/strong&gt;&lt;br&gt;
This is the heart of the project. Each strategy produces a different number and quality of chunks from the same documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Fixed-size Chunking&lt;/strong&gt;&lt;br&gt;
The simplest approach — split every N characters with some overlap regardless of content boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain_text_splitters import CharacterTextSplitter

splitter = CharacterTextSplitter(chunk_size=512, chunk_overlap=50, separator="\n")
chunks = splitter.split_text(text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: 951 chunks&lt;br&gt;
Pros: Fast, simple, predictable&lt;br&gt;
Cons: Cuts across sentences and paragraphs, losing context&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Recursive Character Splitting&lt;/strong&gt;&lt;br&gt;
LangChain's default. Tries to split on paragraph breaks first, then sentences, then words — preserving semantic units as much as possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separators=["\n\n", "\n", ".", " ", ""]
)
chunks = splitter.split_text(text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: 954 chunks&lt;br&gt;
Pros: Smarter splits, respects natural language boundaries&lt;br&gt;
Cons: Still fixed-size, just more intelligent about where to cut&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Semantic Chunking&lt;/strong&gt;&lt;br&gt;
Instead of splitting by size, this approach embeds every sentence and splits where the semantic similarity between adjacent sentences drops below a threshold. Topics stay together, topic boundaries become chunk boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def semantic_chunking(documents, threshold=0.6, min_chunk_size=200):
    # Embed every sentence
    # Split where cosine similarity drops below threshold
    # Merge tiny chunks to ensure minimum size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key insight: The threshold matters enormously. At 0.8 I got 3281 tiny chunks that couldn't answer questions. Lowering to 0.6 produced 1123 meaningful chunks that performed much better.&lt;/p&gt;

&lt;p&gt;Result: 1123 chunks (after tuning)&lt;br&gt;
Pros: Topically coherent chunks, great for complex documents&lt;br&gt;
Cons: Slow (embeds every sentence), sensitive to threshold choice&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Hierarchical Chunking&lt;/strong&gt;&lt;br&gt;
Store small chunks for precise retrieval, but return their larger parent chunk to the LLM for rich context. Best of both worlds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2048, chunk_overlap=50)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)

for parent in parent_splitter.split_text(text):
    for child in child_splitter.split_text(parent):
        chunks.append({"text": child, "parent_text": parent, ...})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During retrieval, the child chunk is used to find the right section, but the parent text is returned to the LLM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if strategy == "hierarchical" and "parent_text" in result.payload:
    text = result.payload["parent_text"]  # Return richer context
else:
    text = result.payload["text"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: 1070 chunks&lt;br&gt;
Pros: Precise retrieval + rich context, perfect faithfulness scores&lt;br&gt;
Cons: More storage, context recall can suffer if parent chunks are too broad&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced RAG Techniques&lt;/strong&gt;&lt;br&gt;
Beyond chunking, I added three techniques to improve retrieval quality:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query Rewriting&lt;/strong&gt;&lt;br&gt;
Before searching, the LLM generates 3 variations of the user's query to capture different aspects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Original: "What was Apple's total revenue in 2023?"
# Rewritten:
# 1. "What was Apple Inc.'s total revenue for fiscal year ending September 2023?"
# 2. "How much revenue did Apple generate during its 2023 fiscal period?"
# 3. "What is Apple's consolidated revenue for the twelve months ending 2023?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each variation searches the vector store independently, results are deduplicated and ranked by score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid Search (Dense + BM25)&lt;/strong&gt;&lt;br&gt;
Combines dense vector search (semantic meaning) with BM25 keyword search (exact term matching). Financial documents have specific numbers and terminology where exact matching helps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Dense search score * 0.7 + BM25 score * 0.3
combined_score = dense_score * 0.7 + bm25_score * 0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Contextual Compression&lt;/strong&gt;&lt;br&gt;
Before passing chunks to the LLM, extract only the sentences relevant to the query. Reduces noise and token usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# From a 500-word chunk about Apple's products and revenue,
# extract only the 2 sentences about revenue figures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Vector Storage with Qdrant&lt;/strong&gt;&lt;br&gt;
I chose Qdrant over ChromaDB for its better performance, built-in hybrid search support, and production-readiness. Running locally via Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -p 6333:6333 qdrant/qdrant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each chunking strategy gets its own collection (2048-dimensional vectors from the NVIDIA embedding model):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COLLECTION_NAMES = {
    "fixed_size": "fixed_size_collection",
    "recursive": "recursive_collection",
    "semantic": "semantic_collection",
    "hierarchical": "hierarchical_collection"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One lesson learned: upsert in batches of 100, not all at once. Sending 1000+ points in a single request causes connection timeouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation Framework&lt;/strong&gt;&lt;br&gt;
I originally planned to use RAGAS but ran into dependency conflicts with the latest version. Instead of spending hours fighting package versions, I built custom LLM-as-judge metrics — which actually gives more control and transparency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 4 Metrics&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Faithfulness —&lt;/strong&gt; Does the answer stick to the retrieved context, or does the model hallucinate?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answer Relevance —&lt;/strong&gt; Does the response actually address the question asked?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context Precision —&lt;/strong&gt; Of what was retrieved, how much was actually relevant?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context Recall —&lt;/strong&gt; Does the context contain enough information to answer the question?&lt;/p&gt;

&lt;p&gt;Each metric prompts the LLM to return a score between 0.0 and 1.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def faithfulness(answer, contexts):
    context = "\n\n".join([c[:300] for c in contexts])
    prompt = f"""Given this context: {context}
And this answer: {answer}
Is the answer fully supported by the context? Reply with just a number: 1.0 for yes, 0.5 for partially, 0.0 for no."""
    return llm_score(prompt)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;LangSmith Tracing&lt;/strong&gt;&lt;br&gt;
Every pipeline run — query, strategy, response, contexts, and all 4 metric scores — is logged to LangSmith automatically. This runs silently in the background and gives a full audit trail of every evaluation run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;&lt;br&gt;
After evaluating all 4 strategies on 5 financial questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Faithfulness&lt;/th&gt;
&lt;th&gt;Ans. Relevance&lt;/th&gt;
&lt;th&gt;Ctx. Precision&lt;/th&gt;
&lt;th&gt;Ctx. Recall&lt;/th&gt;
&lt;th&gt;Overall&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed-size&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.62&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.73&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursive&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.76&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;0.86 🏆&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hierarchical&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.69&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Findings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic chunking wins overall (0.86)&lt;/strong&gt; — After tuning the threshold from 0.8 to 0.6, semantic chunking produced the best faithfulness (0.90) and context recall (0.80). Topically coherent chunks mean the LLM gets focused, relevant context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hierarchical has perfect faithfulness (1.00)&lt;/strong&gt; — Returning parent text to the LLM means it always has rich, complete context to work with. No hallucination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recursive has best context precision (0.89)&lt;/strong&gt; — Smart splitting means retrieved chunks are highly relevant to the query.&lt;/p&gt;

&lt;p&gt;Fixed-size is weakest but simplest — Works fine as a baseline but leaves performance on the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streamlit Chatbot&lt;/strong&gt;&lt;br&gt;
To make the project interactive, I built a Streamlit UI that lets you switch between chunking strategies in real time and see retrieved contexts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strategy = st.selectbox("Chunking Strategy", 
    ["fixed_size", "recursive", "semantic", "hierarchical"])

if prompt := st.chat_input("Ask about Apple's financials..."):
    result = rag_pipeline(prompt, strategy, use_rewriting=True, use_compression=True)
    st.markdown(result["response"])

    with st.expander("Retrieved Contexts"):
        for ctx in result["contexts"]:
            st.markdown(ctx)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with streamlit run app.py. Try asking comparison questions like "How did iPhone revenue change between 2022 and 2023?" to see how different strategies handle multi-document retrieval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons Learned&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;1. The NVIDIA Nemotron model needs special handling&lt;/strong&gt;&lt;br&gt;
The model has a built-in thinking mode. Always set max_tokens=8192 and chat_template_kwargs: {"thinking": False} or you'll get None responses as the model exhausts its token budget on internal reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Semantic chunking threshold is critical&lt;/strong&gt;&lt;br&gt;
Threshold of 0.8 → 3281 tiny, useless chunks. Threshold of 0.6 → 1123 meaningful chunks. Always add a minimum chunk size as a guard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Hierarchical chunking needs parent text for retrieval&lt;/strong&gt;&lt;br&gt;
If you retrieve child chunks but pass child text to the LLM, context recall suffers. Always return the parent text to the LLM while using the child for retrieval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Batch your Qdrant upserts&lt;/strong&gt;&lt;br&gt;
Sending all vectors at once causes connection timeouts. Batch in groups of 100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Build custom eval metrics when RAGAS doesn't cooperate&lt;/strong&gt;&lt;br&gt;
Dependency conflicts are real. Custom LLM-as-judge metrics are transparent, flexible, and work with any model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Evaluation reveals what tuning hides&lt;/strong&gt;&lt;br&gt;
Without evaluation, I would never have caught that semantic was producing tiny useless chunks, or that hierarchical was ignoring parent text. Run eval early and often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full source code available at: &lt;a href="https://github.com/IsaacNatarajan/Advanced-RAG/" rel="noopener noreferrer"&gt;https://github.com/IsaacNatarajan/Advanced-RAG/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built with NVIDIA NIM, Qdrant, LangChain, LangSmith, and Streamlit. If you found this useful, drop a ❤️ and feel free to ask questions in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
