<?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" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Wltsankalpa on Medium]]></title>
        <description><![CDATA[Stories by Wltsankalpa on Medium]]></description>
        <link>https://medium.com/@wltsankalpa?source=rss-4df012e8ed5e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*bUmFKnGOsI_aCAl1y3exjw.jpeg</url>
            <title>Stories by Wltsankalpa on Medium</title>
            <link>https://medium.com/@wltsankalpa?source=rss-4df012e8ed5e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 14:38:16 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@wltsankalpa/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[We Use AI for Coding, Debugging, Design, and Docs. Why Aren’t We 10× Faster?]]></title>
            <link>https://medium.com/@wltsankalpa/we-use-ai-for-coding-debugging-design-and-docs-why-arent-we-10-faster-7f3ffce5a10e?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/7f3ffce5a10e</guid>
            <category><![CDATA[ai-coding-assistant]]></category>
            <category><![CDATA[cursor]]></category>
            <category><![CDATA[developer-productivity]]></category>
            <category><![CDATA[ai-assisted-development]]></category>
            <category><![CDATA[claude]]></category>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Sat, 07 Nov 2026 13:39:47 GMT</pubDate>
            <atom:updated>2026-11-07T13:39:47.107Z</atom:updated>
            <content:encoded><![CDATA[<blockquote><strong>To find a clear answer, I built an illustrative two-week (</strong><strong>80-hour) sprint model from published 2025 and 2026 research telemetry (GitClear, LinearB, Faros AI, Stanford, Microsoft Research, and Google DORA)</strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oV42puUQKkENhRS8OvyKsg.jpeg" /></figure><p>When someone asks why AI coding assistants haven’t made software engineering teams 10 times faster, the standard answer goes like this: <em>“Developers only spend 20% to 30% of their week writing code. Even if AI makes coding 10× faster, the other 70% of the sprint stays the same.”</em></p><p>In 2023, when AI meant single-line autocomplete inside an IDE, that explanation was good enough. In 2026, it no longer holds up.</p><p>Walk past a senior engineer’s desk today and look at how they actually work. They are not just using AI to type functions. When a staging pod crashes, they do not manually dig through dashboards for an hour. Instead, they point an agent (Claude Code, Cursor, or an MCP-connected assistant) at the cluster. The agent runs kubectl get pods, tails the container logs, executes commands inside the pod, port-forwards the service, sends test payloads, connects to PostgreSQL, and runs diagnostic SQL queries to isolate the bad row. When a new business requirement arrives, engineers ask an LLM to draft the system design RFC and sequence diagrams. When a pull request opens, an automated AI code-review bot (CodeRabbit, GitHub Copilot Review, Qodo) posts comments within seconds. Even sprint planning, meeting minutes, and status slide decks are generated by AI.</p><p>Engineers now use AI across <strong>every single stage of the software development lifecycle (SDLC)</strong>, including coding, debugging, testing, system design, sprint planning, code review, and CI/CD operations. So why aren’t engineering teams shipping 5× or 10× faster?</p><p>To understand why and find a real answer, I did not rely on gut feelings, vendor marketing benchmarks, or a single team’s stopwatch log. Instead, I extracted the published time-allocation shares and percentage changes from credible peer-reviewed and industry telemetry studies published in 2025 and 2026 (including <strong>GitClear</strong>, <strong>LinearB</strong>, <strong>Faros AI</strong>, <strong>Stanford University</strong>, <strong>Microsoft Research</strong>, <strong>METR</strong>, and <strong>Google Cloud DORA</strong>) and used them to build an <strong>illustrative two-week (</strong><strong>80-hour) sprint model</strong>.</p><p>When you map those published research percentages onto a single 80-hour sprint to see how all six stages interact, two forces pull in opposite directions:</p><ol><li><strong>The initial speedup is real (</strong><strong>-35.5 hours saved):</strong> Using AI to write initial code, troubleshoot Kubernetes and database issues during debugging, draft design documents, summarize meetings, and run automated PR checks cuts the initial work from <strong>80.0 hours down to </strong><strong>44.5 hours</strong>.</li><li><strong>The cleanup and review rebound eats the savings (</strong><strong>+35.5 hours added back):</strong> Because AI generates output so quickly, it also produces more code duplication, unfamiliar logic that nobody on the team wrote by hand, verbose 10-page design documents that teammates must fact-check, and <strong>twice as many pull requests that are more than twice as large</strong>. Reviewing, debugging, and cleaning up that output adds <strong>+35.5 hours right back (</strong><strong>44.5 h → 80.0 h)</strong>.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Dj1V3UwFYA7_wRTSAlJvfA.png" /><figcaption><em>Figure 1 (Illustrative 80-hour model built from the published stage shares and AI speedup/rebound percentages cited below): Across a two-week sprint (</em><em>10 days × 8 hours = 80.0 hours), using AI across all six stages cuts initial drafting and debugging work to </em><em>44.5 hours (</em><em>-35.5 h saved), before cleaning up over-engineered code, debugging AI-introduced bugs, rewriting generic design docs, and reviewing doubled pull-request queues refill all </em><em>80.0 hour</em></figcaption></figure><h3>1. The Systems Engineering Mechanism: The Pipeline Bottleneck Rule</h3><p>Every backend and platform engineer has seen why local speedups fail to multiply end-to-end throughput: you see the exact mechanism every time you profile a distributed system under load.</p><p>In operations research, Eliyahu Goldratt called this the <strong>Theory of Constraints</strong>. In systems engineering, we know it as the <strong>Pipeline Bottleneck Rule</strong>:</p><blockquote><strong><em>An end-to-end delivery pipeline only moves as fast as its slowest stage. Speeding up stages upstream of the bottleneck does not increase total system throughput. It only builds a deeper work-in-progress (WIP) queue in front of the slowest gate.</em></strong></blockquote><h4>Stage A: Why Speeding Up One Step Hits a Hard Ceiling (1,000 ms → 730 ms)</h4><p>Imagine a backend data job that takes <strong>1,000 ms</strong> end to end when the two steps run one after another:</p><ul><li><strong>300 ms (30%) in the Producer Worker:</strong> Preparing and formatting the data in Python or Node.js.</li><li><strong>700 ms (70%) in the Database (Consumer):</strong> Acquiring locks, validating constraints, and committing the transaction to disk.</li></ul><p>Suppose you rewrite the Producer Worker in Rust and make it <strong>10× faster</strong> (300 ms → 30 ms). Single-job latency does not drop to 100 ms. It drops to <strong>730 ms</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/846/1*oENsbxMAmehxjBzt2XHfcg.png" /></figure><p>Even if you made the Producer Worker <strong>infinitely fast (</strong><strong>0 ms)</strong>, single-job latency is still capped at <strong>700 ms (</strong><strong>1.43× faster)</strong>. And if the producer and database run concurrently as a continuous pipeline, <strong>system throughput is set entirely by the slowest stage</strong>: the database still finishes at most one job every 700 ms, so overall throughput does not improve at all.</p><h4>Stage B: What Happens When Fast Producers Flood the Database (730 ms → 1,000 ms+)</h4><p>In a real system, a 10× faster producer rarely stops at 730 ms. The producer can now generate work far faster than the database can absorb it, and nothing upstream tells it to slow down:</p><ol><li><strong>More jobs arrive:</strong> Producers submit, say, 2× as many write jobs per minute. The database was already the bottleneck, so it cannot keep up.</li><li><strong>Queues form in two places:</strong> Jobs first wait in a queue for a free database connection once the connection pool is full, and then wait inside the database on row or table locks held by other concurrent writers.</li><li><strong>Throughput can drop:</strong> More concurrent writers mean more lock contention, so the database finishes <em>fewer</em> jobs per second than before.</li></ol><p>If the producer also lacks strict validation and deduplication (pushing larger batches or invalid rows that fail and retry), the load multiplies further. Because jobs now arrive faster than the database can complete them, the backlog grows continuously. The 270 ms saved in the producer is quickly erased by connection-pool wait time and lock contention, and end-to-end latency climbs right back to <strong>1,000 ms and beyond</strong>, getting worse the longer the overload lasts.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GUC3dE63N-xQOxfteIz82w.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bguYx67h6mhjeWUwncacSQ.png" /><figcaption><em>Figure 2 (Worked systems analogy vs. illustrative 80-hour sprint model): Speeding up an upstream producer by 10× hits a </em><em>1.37× single-job latency ceiling when the downstream database is unchanged (middle), and loses that gain entirely when faster producers flood the database connection pool and lock queues faster than the database can drain them (right).</em></figcaption></figure><p>That exact producer-consumer bottleneck is what happens inside an AI-assisted software sprint:</p><ul><li><strong>AI is the fast Producer Worker:</strong> It generates code, Kubernetes/SQL debug checks, 10-page design docs, and meeting notes in seconds (cutting initial work from 80.0 hours to 44.5 hours, saving 35.5 hours).</li><li><strong>Human engineers are the Database Consumer Pool (and that pool is shrinking):</strong> Every pull request, architecture decision, and bug fix must still pass through human brains to be understood, reviewed, and verified before shipping to production. At best, the team has the same number of engineers. In reality, many companies are using the <em>promise</em> of AI speedups to freeze hiring or lay off engineers. In our database analogy, that is like <strong>cutting your PostgreSQL connection pool and CPU cores in half at the exact moment upstream producers start firing twice as many jobs</strong>.</li><li><strong>Volume + Quality Multiply the Queue (and Force Rubber-Stamping Into Production):</strong> Unlike a pure Rust rewrite, AI does not just produce work faster. Without strict guardrails, it also produces much larger pull requests, more duplicated code, and more subtle bugs, all landing on a smaller team of surviving reviewers. At the same time, AI code-review bots flood those pull requests with noisy comments about minor syntax and style issues, which takes even more developer time to read and resolve and backs up the review queue further. Faced with a growing pile of large pull requests, overwhelmed engineers either rely on the AI review bot or rubber-stamp approvals without deeply reading the code. When that unreviewed code breaks in production, on-call engineers have to pull logs, build a mental map of unfamiliar AI-written code that <em>neither the author nor the reviewer truly understood</em>, and fix it under live outage pressure.</li></ul><p><strong>Every claim above, including the AI layoff figures, PR review delays, bot noise ratios, and production incident rates, is backed by published 2025 and 2026 industry research and exact numbers in Sections 2 through 5 below.</strong></p><p>In short: when AI makes <em>generating</em> code and documents nearly free, <strong>human review, debugging, and verification</strong> becomes the saturated downstream bottleneck for the entire engineering team.</p><h3>2. How Engineers Spent Their Week Before AI (and Why It Already Shrank From ~30% to ~11 to 16%)</h3><p>Before modeling AI’s impact on a two-week (80-hour) sprint, we need a clear <strong>Pre-AI Baseline</strong> of how software engineers spent their week <em>before</em> LLM tools existed, and how that compares to recent 2024 to 2026 studies:</p><p><strong>Before AI (</strong><strong>2018 to 2021 studies, ~30% hands-on coding):</strong> Even before generative AI existed, writing new code took up roughly one-third of a software engineer&#39;s week:</p><ul><li><a href="https://www.sonarsource.com/blog/how-much-time-do-developers-spend-actually-writing-code/"><strong>Sonar’s 2019 Developer Survey</strong></a><strong>:</strong> Found that developers spent <strong>32% of their workweek</strong> actually writing or improving code. Another <strong>35% of their week</strong> went to code maintenance (19%), testing (12%), and security (4%), while <strong>23% of their week</strong> went to meetings and operational tasks.</li><li><a href="https://stripe.com/files/reports/the-developer-coefficient.pdf"><strong>Stripe’s <em>The Developer Coefficient</em> Report (with Harris Poll)</strong></a><strong>:</strong> Across an average 41.1-hour workweek, developers spent <strong>17.3 hours (</strong><strong>42% of their week)</strong> on maintenance, debugging, refactoring, and fixing bad code (13.5 hours on technical debt and 3.8 hours on bad code), plus time for meetings, design, and code reviews, leaving roughly <strong>~30% of the week</strong> for writing new code.</li><li><a href="https://resources.garden.io/hubfs/Garden_Developer_Productivity_Survey_Report_2021.pdf"><strong>Garden.io’s 2021 Developer Productivity Survey (with Vanson Bourne, </strong></a><a href="https://resources.garden.io/hubfs/Garden_Developer_Productivity_Survey_Report_2021.pdf"><strong>n=400)</strong></a><strong>:</strong> Found that engineers spent <strong>14.3 to 16.5 hours per week (</strong><strong>35% to 40% of their week)</strong> just maintaining CI/CD pipelines, waiting on builds and tests, and fixing internal tooling.</li></ul><p><strong>After AI Adoption (</strong><strong>2024 to 2026 studies, coding shrank to </strong><strong>~10% to 16%):</strong> Once AI coding assistants sped up typing code, the share of the week spent writing code dropped to roughly <strong>one-tenth to one-sixth of the week</strong>, while non-coding work expanded to fill the rest of the 80-hour sprint:</p><ul><li><a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2024/11/Time-Warp-Developer-Productivity-Study.pdf"><strong>Microsoft Research’s <em>Time Warp</em> Study (2024, </strong></a><a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2024/11/Time-Warp-Developer-Productivity-Study.pdf"><strong>n=482 developers)</strong></a><strong>:</strong> Tracking actual workweeks showed that writing code took up just <strong>~11% of a developer&#39;s week</strong> (compared to ~20% in their &quot;ideal&quot; week), alongside meetings and communication (~12%), debugging (~9%), system design (~6%), pull request reviews (~5%), testing, and operations.</li><li><a href="https://www.atlassian.com/blog/bitbucket/devex-bitbucket-pipelines-versus-jenkins"><strong>Atlassian’s 2025 State of DevEx Report</strong></a><strong> (surveying thousands of developers):</strong> Developers reported spending only <strong>16% of their workweek writing code</strong>, while the other <strong>84% of their week</strong> went to searching for context, reviewing code, debugging, attending meetings, and fixing technical debt. Half of the surveyed developers (50%) said they lose <strong>more than 10 hours every week</strong> to organizational and tooling delays.</li><li><a href="https://arxiv.org/pdf/2604.07830"><strong>A 2026 study from Oregon State University and Microsoft Research</strong></a><strong> (<em>“To Copilot and Beyond”</em>, surveying </strong><strong>860 Microsoft engineers):</strong> Confirmed that writing code now takes up roughly <strong>one-tenth (</strong><strong>~10%) of a developer&#39;s day</strong>, while the remaining <strong>~90% of their day</strong> is spent debugging incidents, reviewing pull requests, updating documentation, and aligning designs.</li></ul><p>Notice how those two sets of studies already tell the story: <strong>before AI, hands-on coding was </strong><strong>~30% of the sprint (</strong><strong>24.0 hours out of </strong><strong>80.0 hours, or 3 days out of 10), and after AI it shrank to </strong><strong>~10% (</strong><strong>8.0 hours out of </strong><strong>80.0 hours), while the total 80-hour sprint stayed completely full.</strong> Let us use that exact <strong>30% (</strong><strong>24.0 hours) Pre-AI baseline</strong> from the pre-AI studies and trace where every hour goes across all six stages of the sprint.</p><h3>3. The 80-Hour (10 × 8h) Two-Week Sprint Model: Initial Speedup vs. Net Reality</h3><p>Let us map the published percentages onto one standard two-week sprint (<strong>10 working days × 8 hours = 80.0 hours per engineer</strong>) across four scenarios:</p><ol><li><strong>Pre-AI Baseline (</strong><strong>80.0 h):</strong> Traditional engineering workflow before AI assistants.</li><li><strong>Naive “Coding-Only” 10× Math (</strong><strong>58.4 h):</strong> The classic thought experiment where only coding speeds up by 10× (24.0 h → 2.4 h) and nothing else changes (80.0 / 58.4 = 1.37×).</li><li><strong>Initial AI Speedup Across All 6 Stages (</strong><strong>44.5 h):</strong> What happens when an engineer uses modern AI agents across <strong>all six stages</strong> of the sprint, measuring only the initial time saved while generating code, debugging and troubleshooting incidents, and drafting docs (-35.5 hours saved, or <strong>1.80× faster</strong> on paper before cleanup).</li><li><strong>Real-World AI Sprint After Cleanup &amp; Review (</strong><strong>80.0 h):</strong> What happens once the <strong>cleanup, debugging, and review rebound (</strong><strong>+35.5 hours)</strong> hits each stage.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eJlhThd4TlSInI1hrS_oYA.png" /></figure><p><em>Note: Illustrative 80-hour model constructed by applying the published stage-share percentages and AI speedup/rebound telemetry from the cited studies below, not a single team’s stopwatch log.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b-lsP7a8s2ASP5zsvAwo3w.png" /><figcaption><em>Figure 3 (Illustrative 80-hour model): Every stage of the sprint sees genuine initial time savings from AI tools (</em><em>-35.5 hours on the left), which is offset by a stage-specific cleanup, debugging, and review rebound (</em><em>+35.5 hours on the right).</em></figcaption></figure><h3>4. Deep Dive: What Speeds Up and What Breaks in Every Stage of the Sprint</h3><p>Let us examine the mechanics and published research behind each of the six stages.</p><h4>4.1 Stage 1: Hands-On Coding (24.0 h → 5.0 h Initial → 8.0 h Net)</h4><p><strong>What it is:</strong> Writing new application logic, data models, API endpoints, migrations, and refactoring existing modules inside the IDE.</p><p><strong>Where AI Speeds Things Up (</strong><strong>-19.0 hours)</strong></p><p>Coding assistants and autonomous IDE agents (Cursor, Claude Code, GitHub Copilot, Windsurf) dramatically cut the time required to turn a clear specification into working code. Writing boilerplate, CRUD endpoints, data transformations, and SDK integrations drops from 24.0 hours to roughly <strong>5.0 hours</strong> of prompting, providing context, and steering (-19.0 hours saved).</p><p><strong>Why It Rebounds (</strong><strong>+3.0 hours Cleanup Tax + Downstream Technical Debt)</strong></p><p>When a senior engineer adds a feature to an existing service by hand, they typically inspect neighboring files, extract shared logic into a common helper, and update existing functions in place (<strong>refactoring</strong>).</p><p>LLMs do not naturally work that way. An LLM’s context window is finite, and modifying shared functions carries a risk of breaking callers outside that window. As a result, <strong>AI strongly favors additive, “write-only” code</strong>. It leaves existing functions untouched, duplicates blocks across files, wraps calls in redundant abstraction layers, and silences errors with defensive try/catch blocks or fallback mocks.</p><p>Two large multi-year code-quality studies quantified this shift in 2025 and 2026:</p><p><a href="https://www.gitclear.com/the_ai_code_quality_maintainability_gap"><strong>GitClear’s 2026 Maintainability Gap study</strong></a><strong> (in which GitClear analyzed 623 million code changes from 2023 to 2026, building on </strong><a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research"><strong>their 2025 study of 211 million lines</strong></a><strong>):</strong></p><ul><li><strong>Refactoring dropped by </strong><strong>70%:</strong> The share of changed lines classified as &quot;moved&quot; (which happens when developers extract and reuse shared functions) fell from <strong>21.0% of all code changes in 2022</strong> down to <strong>3.8% in 2026</strong>.</li><li><strong>Updating existing legacy code dropped by </strong><strong>74%</strong> compared to 2022, and <strong>reusing functions across files fell by </strong><strong>35%</strong>.</li><li><strong>Duplicated code blocks jumped by </strong><strong>81%:</strong> Repeated blocks of five or more identical lines hit an all-time high (73.0 duplicate blocks per million changed lines, up from 40.3), while <strong>copy-pasting code within the same commit rose by </strong><strong>41%</strong> (growing from 9.4% of changed lines to 15.7%). Developers are now <strong>roughly 5× more likely</strong> to copy-paste duplicate code than to refactor shared logic.</li><li><strong>Error-hiding wrappers rose by </strong><strong>47%:</strong> <a href="https://www.gitclear.com/industry_stats/ai_code_quality_signal_graphs">GitClear&#39;s live signal graphs</a> show that AI commits contain <strong>47% more defensive error-masking code</strong> (try/catch blocks that swallow exceptions, null-guards, and fallback mocks) that hide broken state instead of failing loudly.</li><li><strong>Early code rewrites (“churn”) rose by </strong><strong>15%:</strong> More newly written lines have to be reverted or rewritten within 14 days of being merged.</li></ul><p><a href="https://assets.linearb.io/image/upload/v1765300694/resources/LinearB_2026_Software_Engineering_Benchmarks_Report.pdf"><strong>LinearB’s 2026 Software Engineering Benchmarks Report</strong></a><strong> (in which LinearB studied 8.1 million pull requests across 4,813 engineering teams):</strong></p><ul><li><strong>Human PRs do </strong><strong>1.5× to </strong><strong>2.2× more refactoring than AI PRs:</strong> In human-written pull requests (at the 75th percentile), <strong>37% of the changed lines</strong> involved refactoring existing code, compared to only <strong>22% in AI-assisted PRs</strong> and <strong>17% in fully agent-generated PRs</strong>.</li><li><strong>AI pull requests are </strong><strong>87% to </strong><strong>160% larger:</strong> Human-written pull requests modified <strong>157 lines of code</strong> (at the 75th percentile), whereas agent-generated PRs modified <strong>293 lines</strong> and AI-assisted PRs modified <strong>408 lines</strong>.</li></ul><p>As one engineer (P208) in the <a href="https://arxiv.org/pdf/2604.07830">2026 Microsoft Research and Oregon State University study</a> put it:</p><blockquote>“We want/need [AI] to do a MUCH better job of analyzing a current code base, architecture, and structure so it can understand how/where to add/extend. Today it loves to duplicate code, and even break existing functionality.”</blockquote><p>Even when a careful engineer spends <strong>+3.0 hours</strong> during coding to strip out dead wrappers and collapse duplicate helpers (bringing net coding time to <strong>8.0 hours</strong>, still a <strong>3.0× speedup</strong> over 24.0 hours), the remaining code bloat flows directly into Debugging (Stage 2) and PR Review (Stage 5).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nK9BI5x90xHuOtfd-ZvJmA.png" /><figcaption><em>Figure 4: Human engineers consolidate shared logic (</em><em>0.37 refactor rate, </em><em>157 LOC at P75 in LinearB&#39;s dataset), whereas AI coding tools default to additive &quot;write-only&quot; generation (</em><em>-70% refactoring, </em><em>+81% block duplication, </em><em>+47% error-masking guards, and </em><em>293 to 408 LOC diffs in GitClear and LinearB data).</em></figcaption></figure><p>In short: AI makes writing initial code ~3× to 5× faster, but shifts the codebase from modular refactoring to copy-paste duplication.</p><h4>4.2 Stage 2: Debugging, K8s/DB Investigation &amp; Testing (12.0 h → 6.5 h Initial → 14.5 h Net)</h4><p><strong>What it is:</strong> Investigating staging and production bugs, inspecting Kubernetes pods and logs, querying databases, reproducing failures, and writing unit and integration tests.</p><p><strong>Where AI Speeds Things Up (</strong><strong>-5.5 hours)</strong></p><p>Modern engineers rarely debug from a blank terminal. Using CLI agents (Claude Code, Cursor agent mode, or custom MCP servers), an engineer can paste a stack trace or alert and let the agent:</p><ol><li>Run kubectl get pods, kubectl describe, and kubectl logs across namespaces.</li><li>Execute diagnostic commands inside containers (kubectl exec) or open local port-forwards (kubectl port-forward) to curl live endpoints.</li><li>Connect to staging or read-replica databases, inspect table schemas, and run SQL queries to check data state.</li><li>Read CI/CD failure logs and generate unit or integration test files.</li></ol><p>Automating this troubleshooting, incident triage, and test-setup work cuts initial investigation time nearly in half, from 12.0 hours down to <strong>6.5 hours (</strong><strong>-5.5 hours saved)</strong>. In <a href="https://arxiv.org/pdf/2604.07830">Microsoft Research&#39;s 2026 survey of 860 engineers</a>, <strong>40.6% of infrastructure engineers</strong> and <strong>19.5% of application developers</strong> ranked automated incident troubleshooting and diagnostics as one of the most helpful uses of AI: <em>&quot;There are lots of logistics that need to be done... if those are done by AI, it would be a great win.&quot;</em></p><p><strong>Why It Rebounds (</strong><strong>+8.0 hours from Unfamiliar Code, False Leads &amp; Escaped Bugs)</strong></p><p>If AI makes initial troubleshooting and test scaffolding 5.5 hours faster, why does total debugging and testing time <em>increase</em> from 12.0 hours to <strong>14.5 hours (</strong><strong>+2.5 hours net)</strong>? Three factors drive an <strong>+8.0 hour rebound</strong>:</p><ol><li><strong>The Missing-Mental-Map Penalty (</strong><strong>+3.5 hours): </strong>When you write a 300-line service by hand, you build an internal mental map of its state transitions, locking assumptions, and edge cases while typing. When an AI agent writes those 300 lines in 30 seconds, <strong>nobody on the team has that mental map</strong>. When a subtle race condition or data-corruption bug triggers in staging three days later, you are not debugging <em>your</em> code. You are reverse-engineering a stranger&#39;s code under incident pressure. Worse, because <a href="https://www.gitclear.com/the_ai_code_quality_maintainability_gap">GitClear&#39;s 2026 study</a> found AI increases <strong>error-masking wrappers by </strong><strong>+47%</strong> (swallowed exceptions and silent fallback values), the crash happens far downstream from where the state actually went wrong.</li><li><strong>Confident Root-Cause Hallucinations (</strong><strong>+2.5 hours): </strong>Agents are fast at collecting kubectl logs and SQL rows, but unreliable at causal root-cause reasoning across distributed services. When an agent correlates three log lines and presents a plausible-sounding (but wrong) root-cause theory with 100% confidence, engineers waste hours chasing a dead end. As one Microsoft engineer (P223) explained in the <a href="https://arxiv.org/pdf/2604.07830">To Copilot and Beyond study</a>: <em>“AI is very bad at understanding the context and throwing wrong answers with utmost confidence, makes us waste time instead of helping.”</em></li><li><strong>Self-Passing Unit Tests &amp; Rising Bug Rates (</strong><strong>+2.0 hours): </strong>In that same <a href="https://arxiv.org/pdf/2604.07830">Microsoft Research study</a>, <strong>44.5% of respondents</strong> in the testing track pointed out a common trap in AI test generation. When you ask an LLM to write unit tests for code it just generated, it reads the implementation (including its bugs) and writes tests that lock in the buggy behavior, or mocks out the exact database/network boundary where the bug lives. The test suite shows 100% green passing checks in CI while letting real bugs slip through.</li></ol><p><strong>Rising Bug Rates in Industry Studies:</strong> In <a href="https://243608892.fs1.hubspotusercontent-na2.net/hubfs/243608892/AI_Engineering_Impact_Report_July_2025_Faros_AI.pdf">Faros AI’s July 2025 study of 10,000+ developers</a>, high AI adoption correlated with a <strong>+9% increase in bugs per developer</strong>. In <a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025">Faros AI&#39;s 2026 follow-up study across 22,000 developers</a>, <strong>bugs per developer climbed to </strong><strong>+54%</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j-1Pe_ryVxjfhO5Ej1wzjw.png" /><figcaption><em>Figure 5: Agents save </em><em>5.5 hours automating </em><em>kubectl, pod logs, SQL queries, and test setup (left), but add </em><em>8.0 hours back because engineers must debug unfamiliar AI code without a mental map, check confident false root-cause theories, and fix </em><em>+54% more bugs (right).</em></figcaption></figure><p>In short: AI agents automate the <em>legwork</em> of debugging (kubectl, SQL, log search), but AI-written code produces 54% more bugs that take longer to diagnose because no human holds the mental model.</p><h4>4.3 Stage 3: System Design &amp; Architecture RFCs (12.0 h → 8.5 h Initial → 14.0 h Net)</h4><p><strong>What it is:</strong> Translating business requirements into system architecture, API contracts, database schemas, Request for Comments (RFC) design documents, and trade-off analyses.</p><p><strong>Where AI Speeds Things Up (</strong><strong>-3.5 hours)</strong></p><p>Instead of staring at a blank page, an engineer feeds product requirements and existing schema files into an LLM and asks for an RFC draft, sequence diagrams, and failure-mode tables. Initial drafting drops from 12.0 hours to <strong>8.5 hours (</strong><strong>-3.5 hours saved)</strong>.</p><p><strong>Why It Rebounds (</strong><strong>+5.5 hours &quot;Workslop&quot; &amp; Spec-Restart Tax)</strong></p><p>Here we run into a problem documented in recent workplace research: <strong>AI-generated “workslop.”</strong></p><p>In September 2025, researchers from <strong>Stanford University’s Social Media Lab and BetterUp Labs</strong> published a <a href="https://hbr.org/2025/09/ai-generated-workslop-is-destroying-productivity">study in <em>Harvard Business Review</em></a> (with full details in the <a href="https://www.betterup.com/hubfs/Post-Uplift%20content/BetterUp_Uplift_The%20hidden%20cost%20of%20workslop.pdf">BetterUp and Stanford research report</a>) surveying <strong>1,150 full-time U.S. desk workers</strong>:</p><ul><li><strong>What “Workslop” Means:</strong> AI-generated content (such as design documents, technical reports, slide decks, summaries, and code) that looks polished, structured, and complete at a glance, but lacks the project-specific context, constraints, and substance required to actually move the work forward.</li><li><strong>How Common It Is:</strong> <strong>40% of surveyed employees</strong> reported receiving &quot;workslop&quot; from coworkers in the past month, <strong>53% of employees</strong> admitted to sending it themselves at least occasionally, and employees estimated that <strong>15.4% of all work they receive</strong> at their company is now AI-generated workslop.</li><li><strong>The Transfer Tax (</strong><strong>1 hour 56 minutes per incident):</strong> As the Stanford and BetterUp researchers explain, <em>&quot;The insidious effect of workslop is that it shifts the burden of the work downstream, requiring the receiver to interpret, correct, or redo the work. In other words, it transfers the effort from creator to receiver.&quot;</em> On average, teammates on the receiving end spent <strong>1 hour and 56 minutes per workslop document</strong> reading, fact-checking, and rewriting the material.</li><li><strong>Impact on Team Trust:</strong> Over half of the people who received workslop said it damaged their opinion of the sender: <strong>54% of recipients</strong> viewed the sender as less creative, <strong>50% of recipients</strong> saw them as less capable, <strong>49%</strong> saw them as less reliable, and <strong>42%</strong> trusted them less.</li></ul><p>In software architecture, design workslop is especially expensive:</p><ul><li><strong>Generic Textbook Patterns &amp; Unstated Assumptions:</strong> In <a href="https://arxiv.org/pdf/2604.07830">Microsoft Research’s 2026 study</a>, engineers noted that when asked to design a system, LLMs default to verbose, generic patterns that ignore local infrastructure constraints (<em>“AI would probably fall back to using the same known pattern regardless of the design particularities. That makes the designs inefficient”</em>) and silently invent requirements or assume guarantees that do not exist.</li><li><strong>+13.8% More Task Restarts:</strong> When an engineer prompts a 10-page RFC in 10 minutes without thinking through edge cases, teammates either spend hours in design review stripping out filler and checking assumptions, or approve it superficially. <a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025">Faros AI’s 2026 study</a> found that <strong>task restarts</strong> (tickets that move backward to <em>in-progress</em> after hitting an architectural dead end) <strong>increased by </strong><strong>+13.8%</strong>.</li></ul><p>Fact-checking and rewriting bloated AI RFCs (~2 hours per doc across 2 to 3 specs per sprint) plus re-architecting restarted tasks adds <strong>+5.5 hours</strong> back, pushing net design time from 12.0 hours to <strong>14.0 hours (</strong><strong>+2.0 hours net increase)</strong>.</p><p>In short: prompting a 10-page design doc saves the author 30 minutes and costs the rest of the engineering team 2 hours to read, fact-check, and rewrite.</p><h4>4.4 Stage 4: Sprint Planning, Meetings &amp; Presentations (12.0 h → 8.5 h Initial → 12.0 h Net)</h4><p><strong>What it is:</strong> Sprint planning, backlog refinement, daily standups, writing meeting minutes, preparing architecture/demo slide decks, and updating documentation.</p><p><strong>Where AI Speeds Things Up (</strong><strong>-3.5 hours)</strong></p><p>AI meeting transcription bots summarize 60-minute calls automatically, presentation tools generate slide decks from bullet points, and task agents break high-level epics into Jira subtasks. That trims roughly <strong>3.5 hours</strong> of manual note-taking and slide formatting (12.0 h → 8.5 h).</p><p><strong>Why It Rebounds (</strong><strong>+3.5 hours Document Inflation &amp; Multi-Agent Context Switching)</strong></p><p>All 3.5 hours saved in creating notes and tickets are consumed by two new coordination taxes:</p><p><strong>Ticket &amp; Documentation Inflation: </strong>When generating Jira tickets, status reports, and wiki pages takes zero effort, volume explodes. Instead of a concise 5-line ticket or a 4-bullet meeting decision log, engineers face 15 auto-generated subtasks and 3-page AI meeting summaries where key decisions are buried inside generic filler. In the <a href="https://arxiv.org/pdf/2604.07830">2026 Microsoft Research survey</a>, <strong>45.9% of respondents</strong> in the Meta-Work track flagged documentation drift and AI doc bloat as a major pain point: <em>“Unfortunately, I think right now the focus is on just generating a bunch of AI slop that nobody will ever read or maintain, which just makes all the documentation problems we have now even worse.”</em></p><p><strong>Parallel Workstream Explosion (</strong><strong>+67.4% PR Contexts &amp; </strong><strong>+26% Stalled Work): </strong>Because coding agents can run in the background, developers spin up multiple tasks at the same time. <a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025">Faros AI’s 2025 and 2026 studies</a> measured the cost of this multitasking:</p><ul><li>Developers on high-AI teams interact with <strong>+47% more PRs per day</strong> in 2025, rising to <strong>+67.4% more PR contexts</strong> and <strong>+17.7% more task contexts per day</strong> in 2026.</li><li><strong>Stalled tasks (</strong><strong>7+ days in-progress with zero PR activity) jumped </strong><strong>+26%</strong>. Starting work with an AI agent is easy. Finishing, integrating, and shipping all those parallel threads requires extra standup coordination, blocker triage, and re-planning (+3.5 hours), leaving net planning and meeting time right back at <strong>12.0 hours (</strong><strong>0.0 h net savings)</strong>.</li></ul><p>In short: AI writes the meeting notes and Jira tickets faster, but managing +67% more parallel work-in-progress and +26% more stalled tickets eats every minute saved.</p><h4>4.5 Stage 5: PR Code Review &amp; AI Review Bots (10.0 h → 8.0 h Initial → 18.5 h Net)</h4><p><strong>What it is:</strong> Reviewing teammates’ pull requests, responding to review comments, verifying business logic and security, and waiting for merge approval.</p><p><em>“Wait,”</em> many engineers ask, <em>“if human PR review is the bottleneck, don’t AI code review agents (</em><em>CodeRabbit, </em><em>GitHub Copilot Review, </em><em>Qodo, </em><em>Cursor Bugbot, </em><em>Greptile) automate code reviews too?&quot;</em></p><p>Let us look at what happened when researchers measured AI code review agents (CRAs) in real repositories in 2025 and 2026.</p><p><strong>Where AI Speeds Things Up (</strong><strong>-2.0 hours)</strong></p><p>AI code review bots are genuinely helpful as a <strong>first-pass checker</strong>. They summarize diffs, flag obvious null-pointer risks, catch missing error checks, and point out style violations before a human reviewer looks at the PR (-2.0 hours saved, 10.0 h → 8.0 h). In controlled deployments with strict noise filtering, such as the <a href="https://arxiv.org/html/2508.09676">DeputyDev double-controlled A/B study at TATA 1mg across 200 engineers</a>, contextual pre-review reduced per-line review duration.</p><p><strong>Why It Explodes (</strong><strong>+10.5 hours from Bot Noise + </strong><strong>2× Volume + </strong><strong>+154% Size + </strong><strong>5.3× Pickup Wait)</strong></p><p>Across the broader industry, three independent studies show why PR code review has become the <strong>#1 bottleneck in modern software engineering</strong>, climbing from 10.0 hours to <strong>18.5 hours (</strong><strong>+85% increase)</strong> per sprint:</p><p><a href="https://arxiv.org/html/2604.03196"><strong>An April 2026 MSR study of 3,109 Pull Requests</strong></a><strong> (<em>“From Industry Claims to Empirical Reality: An Empirical Study of Code Review Agents in Pull Requests”</em>):</strong><br>Researchers analyzed 19,450 review records across 3,109 pull requests from the AIDev dataset:</p><ul><li>Vendor marketing claimed AI review bots could handle 80% of pull requests without human involvement. In reality, pull requests reviewed only by AI bots were approved and merged <strong>45.20% of the time</strong>, compared to <strong>68.37% for human-reviewed pull requests</strong>, and <strong>34.88% of bot-reviewed PRs were abandoned</strong> (versus 21.60% for human-reviewed PRs).</li><li>Why? Because most bot comments were noise. In <strong>60.2% of closed bot-reviewed pull requests</strong>, <strong>fewer than 1 in 3 bot comments (</strong><strong>0% to 30%) pointed to a real bug, security flaw, or design issue</strong>. Out of 13 AI code-review bots tested, <strong>12 bots had a useful-comment rate below </strong><strong>60%</strong> (for example, only 19.8% of comments from GitHub Copilot Review, 27.6% from GitHub Advanced Security Bot, and 43.4% from Cursor Bot led to actual code fixes).</li><li>A <a href="https://dl.acm.org/doi/abs/10.1145/3793302.3793599">companion MSR 2026 study of 7,416 reviewer-bot comments across 4,532 pull requests</a> confirmed that <strong>the more comments an AI review bot posts on a pull request, the longer that pull request takes to merge</strong>.</li></ul><p><a href="https://arxiv.org/html/2412.18531v2"><strong>An industrial study of 4,335 Pull Requests by Bilkent University and Beko</strong></a><strong> (<em>“Automated Code Review In Practice”</em>):</strong><br>Researchers studied <strong>238 developers across 10 industrial projects and 4,335 pull requests</strong> (1,568 of which were reviewed by an LLM code-review bot built on Qodo PR-Agent and GPT-4):</p><ul><li>Even though developers marked 73.8% of the bot&#39;s comments as resolved, <strong>average pull request merge time took </strong><strong>42% longer after introducing the bot, rising from </strong><strong>5 hours 52 minutes to </strong><strong>8 hours 20 minutes</strong>.</li><li>Developers reported that reading, arguing with, or making unnecessary code edits just to satisfy context-blind bot comments slowed down their reviews.</li></ul><p><a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025"><strong>Faros AI’s 22,000-developer study</strong></a><strong> &amp; </strong><a href="https://assets.linearb.io/image/upload/v1765300694/resources/LinearB_2026_Software_Engineering_Benchmarks_Report.pdf"><strong>LinearB’s 8.1M PR benchmarks</strong></a><strong>:</strong><br>Because AI coding tools allow developers to open <strong>nearly twice as many pull requests (</strong><strong>+98%)</strong> that are <strong>more than twice as large (</strong><strong>+154% more lines changed)</strong>, human reviewers get buried:</p><ul><li><strong>Time spent in PR review jumped by </strong><strong>91%</strong> in <a href="https://243608892.fs1.hubspotusercontent-na2.net/hubfs/243608892/AI_Engineering_Impact_Report_July_2025_Faros_AI.pdf">Faros AI&#39;s 2025 report</a> (10,000+ developers) and <strong>jumped by </strong><strong>441% (more than 5× longer median review time)</strong> in <a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025">Faros AI&#39;s 2026 report</a> (22,000 developers).</li><li><strong>PRs wait </strong><strong>5.3× longer for a reviewer to open them:</strong> <a href="https://assets.linearb.io/image/upload/v1765300694/resources/LinearB_2026_Software_Engineering_Benchmarks_Report.pdf">LinearB&#39;s 2026 study of 8.1M PRs</a> found that pull requests written by AI agents sit untouched in the review queue for <strong>17.6 hours (</strong><strong>1,055 minutes at the 75th percentile)</strong> before a human reviewer even opens them, compared to <strong>8.3 hours (</strong><strong>496 minutes)</strong> for AI-assisted PRs and just <strong>3.35 hours (</strong><strong>201 minutes)</strong> for human-written PRs.</li><li><strong>Much lower approval rates:</strong> LinearB found that <strong>84.4% of human-written pull requests get approved and merged</strong>, whereas <strong>only </strong><strong>32.7% of AI-generated pull requests get merged</strong>. Engineering leaders surveyed by LinearB explained why:</li></ul><p><em>“AI generates advanced code the author could not explain.”</em><br><em>“Without context it generates more work than a junior developer. The workload increases for the reviewer.”</em></p><p><strong>Shrinking Reviewer Pools From AI-Justified Layoffs &amp; Hiring Freezes:</strong><br>To make this review logjam even worse, the number of human reviewers on the team is often shrinking rather than growing. Believing that AI makes engineering teams multiple times faster, many companies have frozen hiring or laid off developers:</p><ul><li><a href="https://leaddev.com/wp-content/uploads/2025/08/THE-AI-IMPACT-REPORT-2025-download__LDMO__.pdf"><strong>LeadDev’s 2025 AI Impact Report</strong></a><strong> (surveying </strong><strong>883 engineering leaders and developers):</strong> Found that <strong>26% of surveyed companies had already reduced or combined their engineering teams</strong> after adopting AI, and <strong>54% of surveyed engineering leaders predicted that companies will hire fewer junior developers</strong> in the future.</li><li><a href="https://www.challengergray.com/wp-content/uploads/2026/01/Challenger-Report-December-2025.pdf"><strong>Challenger, Gray &amp; Christmas Job Cut Reports (2025 &amp; 2026)</strong></a><strong> &amp; </strong><a href="https://ailayoffs.org/research/counting-ai-layoffs-2026-10.pdf"><strong>The Counting AI Layoffs Register</strong></a><strong> (tracking 243 layoff events from 2023 to 2026):</strong> U.S. employers cited AI as the reason for <strong>54,836 announced job cuts in 2025</strong> (out of 154,445 total tech job cuts) and over <strong>120,000 AI-cited job cuts through September 2026</strong>, with <strong>software engineering and IT roles cut most often</strong>.</li><li>When a company reduces its engineering headcount while the remaining developers use AI to open twice as many pull requests that are more than twice as large, every surviving engineer is left with a much deeper review, debugging, and on-call queue.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kovztsRx0NAqq4Boea9Bjg.png" /><figcaption><em>Figure 6: In both system design (top) and pull request review (bottom), AI makes generating drafts effortless for the author while shifting a heavy review and noise-filtering burden onto teammates (</em><em>1h 56m per workslop doc; </em><em>&lt;30% bot signal; </em><em>5.3× longer PR pickup wait; </em><em>+91% to </em><em>+441% longer review cycles).</em></figcaption></figure><p>In short: AI code review bots cannot approve architectural intent, noisy bot comments add +42% to PR closure time, and human reviewers face 2× as many PRs that are 154% larger. That nearly doubles sprint review hours (10.0 h → 18.5 h).</p><h4>4.6 Stage 6: CI/CD Pipelines, Deployments &amp; Production Ops (10.0 h → 8.0 h Initial → 13.0 h Net)</h4><p><strong>What it is:</strong> Maintaining CI/CD workflows, waiting on build/test pipelines, resolving merge conflicts, deploying releases, and handling on-call production incidents.</p><p><strong>Where AI Speeds Things Up (</strong><strong>-2.0 hours)</strong></p><p>AI assistants save roughly <strong>2.0 hours</strong> per sprint (10.0 h → 8.0 h) by generating GitHub Actions workflows, Dockerfiles, Helm charts, and Terraform configurations, and by summarizing long CI build failure logs. In <a href="https://arxiv.org/pdf/2604.07830">Microsoft Research&#39;s 2026 developer survey</a>, 33.7% of infrastructure respondents specifically requested CI/CD blueprint and log-diagnosis assistants.</p><p><strong>Why It Rebounds (</strong><strong>+5.0 hours CI Queue Contention &amp; </strong><strong>+242.7% Production Incidents)</strong></p><p><strong>CI Pipeline Contention &amp; Merge Conflicts (</strong><strong>+2.0 hours):</strong><br>When a team merges <strong>+98% more PRs</strong> that touch more files per commit (4.95 to 7.51 files and 293 to 408 lines at P75 in <a href="https://assets.linearb.io/image/upload/v1765300694/resources/LinearB_2026_Software_Engineering_Benchmarks_Report.pdf">LinearB&#39;s 2026 report</a>), CI build queues back up, integration test suites run twice as often, and branches fall out of date faster. Engineers spend extra hours rebasing and resolving merge conflicts.</p><p><strong>The “Acceleration Whiplash” in Production (</strong><strong>+3.0 hours):</strong><br>What happens when reviewers are overwhelmed by 2× PR volume and 154% larger diffs? LinearB&#39;s survey and <a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025">Faros AI&#39;s 2026 study</a> captured the exact failure mode: <strong>reviewer fatigue leads to rubber-stamping.</strong></p><ul><li>In Faros AI’s 2026 study across 22,000 developers, <strong>31% more PRs merged with zero human review at all</strong>.</li><li>As a direct consequence, <strong>production incidents per PR jumped </strong><strong>+242.7%</strong>. Every merged code change is now <strong>more than </strong><strong>3.4× as likely to trigger a production incident</strong>.</li></ul><p>On-call incident triage, hotfixes, and rollbacks add <strong>+3.0 hours</strong> per sprint, bringing net CI/CD and production ops time from 10.0 hours to <strong>13.0 hours (</strong><strong>+3.0 hours net increase)</strong>.</p><p>In short: AI helps write the YAML, but pushing twice as many oversized, lightly reviewed PRs through CI/CD triples the production incident rate per PR.</p><h3>5. The Perception Gap: Why 1.21× Feels Like 10×</h3><p>Net completed tasks per sprint only rise by <strong>+21% to </strong><strong>+34% (</strong><strong>1.21× to 1.34×)</strong>, and company-wide DORA delivery metrics remain flat or suffer stability whiplash. So why do so many developers sincerely feel like they are moving 5× or 10× faster?</p><p>In <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">a July 2025 randomized controlled trial by METR</a> (with <a href="https://metr.org/blog/2026-02-24-uplift-update/">a February 2026 follow-up</a>), researchers ran a controlled experiment with experienced open-source developers:</p><p><strong>Study Design:</strong> 16 experienced maintainers working on large, mature open-source repositories they knew deeply (averaging <strong>22,000+ GitHub stars</strong> and <strong>1,000,000+ lines of code</strong>) completed <strong>246 real repository issues</strong> (features, bug fixes, refactors averaging 2 hours each). Issues were randomly assigned to either allow frontier AI tools (primarily Cursor Pro with Claude 3.5/3.7 Sonnet) or disallow AI, with full screen recording.</p><p><strong>The Perception vs. Reality Gap:</strong></p><ul><li><strong>Before starting:</strong> Developers forecasted that AI would speed them up by <strong>+24%</strong>.</li><li><strong>After finishing:</strong> Even after completing the tasks, developers believed AI had sped them up by <strong>+20%</strong>.</li><li><strong>Actual Stopwatch Time:</strong> In the early-2025 trial, developers using AI actually took <strong>19% longer</strong> to complete issues than when working without AI (in the <a href="https://metr.org/blog/2026-02-24-uplift-update/">February 2026 follow-up cohort</a>, measured speedup shifted to <strong>4% to 18% faster</strong>, still nowhere near 2× or 10×).</li></ul><p>Why do smart engineers misjudge their own stopwatch time by 35 to 40 percentage points?</p><ol><li><strong>Generating Feels Fast; Reviewing and Fixing Feels Invisible:</strong><br>Watching an agent generate 400 lines of code, a 10-page RFC, or a complex SQL query in 15 seconds feels effortless. Your brain anchors on the 15-second generation burst (44.5 hours). You do not mentally bill &quot;AI&quot; when you spend 45 minutes two days later untangling a subtle race condition, resolving a noisy bot comment, or fixing a broken test (+35.5 hours).</li><li><strong>Mechanical vs. Deep-Thinking Work:</strong><br>A <a href="https://dora.dev/insights/customization-supports-developer-engagement/">UC Berkeley eye-tracking study highlighted by Google Cloud DORA</a> found that developers gave <strong>19% of their visual attention</strong> to AI chat during mechanical tasks (writing boilerplate, installing packages), but <strong>less than </strong><strong>1% visual attention</strong> during complex code understanding tasks. When deep comprehension was required, engineers chose to read and reason manually to stay in control.</li><li><strong>Bounded Delegation:</strong><br>In <a href="https://arxiv.org/pdf/2604.07830">Microsoft Research’s 2026 paper</a>, the authors call this boundary <strong>“bounded delegation”</strong>: developers want AI to handle the <em>setup and legwork surrounding their craft</em> (initial incident diagnostics, scaffolding tests, formatting docs), while keeping human ownership over <em>architectural judgment, design trade-offs, and production sign-off</em>.</li></ol><h3>6. The Engineering Playbook: How to Keep the 35.5 Hours AI Saves</h3><p>The findings from Faros AI, LinearB, GitClear, and DORA make one thing clear: <strong>the </strong><strong>35.5 hours of initial AI time savings across an 80-hour sprint are real.</strong> Teams lose those 35.5 hours because they deploy 10× generation tools without engineering guardrails for <strong>verbose AI chat responses, PR batch size, code reuse, or document bloat</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bfq3TuwCU2DPpMucUdI_5w.png" /><figcaption><em>Figure 7: Without guardrails, full-lifecycle AI generation floods the human review gate and triggers the cleanup and review rebound (Path A, top right). High-performing engineering teams enforce practical guardrails across all six stages to keep the hours AI saves (Path B, bottom right).</em></figcaption></figure><h4>6.1 Stopping Verbose “Walls of Text” Inside Your Coding Assistant</h4><p>The “workslop” problem does not start when someone opens a 10-page RFC or a 400-line pull request. <strong>It starts inside your own IDE chat pane.</strong></p><p>By default, coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, Gemini) are trained to reward thorough-looking answers. Ask a one-sentence question or request a small edit, and the assistant often replies with <strong>five paragraphs of nitty-gritty detail</strong>: narrating every file it opened, repeating the diff in prose, and burying the one architectural decision that actually matters inside paragraph four. The details are not necessarily wrong, but dumping them all at once overwhelms working memory and destroys your high-level mental map.</p><p>Four harness-level controls stop in-chat walls of text before they happen:</p><p><strong>1. Flip the built-in verbosity switches:</strong> In <strong>Claude Code</strong>, run /output-style concise (which strips conversational preamble, narration, and closing recaps from the system prompt; see <a href="https://code.claude.com/docs/en/output-styles">Claude Code Output Styles</a>). In <strong>OpenAI Codex CLI</strong>, set model_verbosity = &quot;low&quot; and hide_agent_reasoning = true in ~/.codex/config.toml (<a href="https://file+.vscode-resource.vscode-cdn.net/Users/tharindu/git/pgvector-filtering-investigation/ai-sprint-bottleneck/toolkit/codex-config.toml">toolkit/codex-config.toml</a>).</p><p><strong>2. Replace vague adjectives (“be concise”) with strict Output Shape Rules (BLUF + Progressive Zoom):</strong> Telling an LLM <em>“please be brief”</em> rarely sticks. Instead, put a structural <strong>Bottom Line Up Front (BLUF)</strong> rule in CLAUDE.md, AGENTS.md, GEMINI.md, or .cursor/rules/ that forces <strong>three explicit zoom levels</strong> (<a href="https://file+.vscode-resource.vscode-cdn.net/Users/tharindu/git/pgvector-filtering-investigation/ai-sprint-bottleneck/toolkit/skills/progressive-zoom/SKILL.md">toolkit/skills/progressive-zoom/SKILL.md</a>):</p><ul><li><strong>Zoom 1 (The 30-Second Mental Map: Default):</strong> Open with a 1 to 2 sentence conclusion, a compact 5-node ASCII/Mermaid flow diagram (/visualize in Cursor), and at most 3 bullets naming the key file:line boundaries. <strong>Then stop</strong> and ask whether to zoom into <strong>Mechanism &amp; State Flow (Zoom 2)</strong> or <strong>Line-by-Line Code (Zoom 3)</strong>.</li><li><strong>Post-edit silence:</strong> After modifying code, list only the touched file:line paths (one line each). Never narrate the code diff in chat when the diff is already visible in the editor.</li></ul><p><strong>3. Isolate noisy multi-file searches and log dumps in Subagents:</strong> As <a href="https://claude.com/resources/articles/steering-claude-code-skills-hooks-rules-subagents-and-more">Anthropic’s 2026 guide on steering Claude Code</a> explains, whenever your main chat agent reads 15 files or tails kubectl logs, all that raw noise stays in your main context window and gets regurgitated back to you. Delegate codebase tracing and log inspection to an isolated <strong>Subagent</strong> (<a href="https://file+.vscode-resource.vscode-cdn.net/Users/tharindu/git/pgvector-filtering-investigation/ai-sprint-bottleneck/toolkit/agents/mental-map-explorer.md">toolkit/agents/mental-map-explorer.md</a>) with a hard contract: <em>&quot;Return at most 15 lines containing a 3-sentence summary, a 1-line call-chain map, and the 5 essential </em><em>file:line locations.&quot;</em></p><p><strong>4. Use Plan Mode (</strong><strong>Shift+Tab) and 3-Tier Spec Discipline:</strong> Both <a href="https://academy.claude.com/courses/claude-code-101/the-explore-plan-code-commit-workflow">Claude Code</a> and <a href="https://specroot.io/notes/plan-mode-vs-spec-driven-development/">Cursor</a> separate exploration from editing: if you can describe a diff in one sentence, run the agent directly; if it touches multiple files in one session, press Shift+Tab into <strong>Plan Mode</strong> so you review and trim a 5-bullet plan before any code is written; if the work spans multiple sessions or teammates, commit a 1-page spec.md first.</p><h4>6.2 Stage-by-Stage Engineering Mitigations Across the 80-Hour Sprint</h4><p>Beyond the chat UI, here is how high-performing teams counteract the specific AI bottleneck in each of the six sprint stages, backed by current engineering tools and research:</p><p><strong>Stage 1 Mitigation (Hands-On Coding: Blocking Clones &amp; Silent Error Masking):</strong></p><ul><li><strong>Gate CI on <em>new</em> code clones with </strong><strong>jscpd:</strong> Counteract AI&#39;s +81% block-duplication habit (<a href="https://www.gitclear.com/the_ai_code_quality_maintainability_gap">GitClear 2026</a>) by adding a fast token-based clone check in CI that fails <strong>only when a PR introduces new duplicated blocks</strong> compared to main (<a href="https://jscpd.dev/guides/ci">jscpd CI Guide</a>):</li></ul><pre>npx jscpd . --baseline-from-ref origin/main --fail-on-new-clones --reporters ai</pre><ul><li><strong>Run a post-coding De-Bloat &amp; Silent-Failure Subagent:</strong> Before committing, run an automated pass modeled on Anthropic’s official <a href="https://github.com/anthropics/claude-plugins-official/tree/main/plugins/code-simplifier">code-simplifier</a> and <a href="https://github.com/anthropics/claude-plugins-official/tree/main/plugins/pr-review-toolkit">silent-failure-hunter</a> agents (<a href="https://file+.vscode-resource.vscode-cdn.net/Users/tharindu/git/pgvector-filtering-investigation/ai-sprint-bottleneck/toolkit/agents/debloat-and-silent-failure-auditor.md">toolkit/agents/debloat-and-silent-failure-auditor.md</a>) to collapse duplicate helpers and strip out the +47% broad try/catch blocks and fallback mocks that hide broken state.</li></ul><p><strong>Stage 2 Mitigation (Debugging &amp; Testing: Killing Self-Passing AI Tests with Mutation Feedback):</strong></p><ul><li><strong>Why line coverage lies on AI tests:</strong> In an October 2026 benchmark of 165 applications (<a href="https://arxiv.org/html/2610.13065"><em>TestSuiteBench</em>, ETH Zurich</a>), researchers found that even when 88% of agent-generated unit tests ran and passed with high line coverage, they achieved <strong>below a </strong><strong>60% mutation score</strong>: meaning the tests failed to catch artificial bugs injected into the code.</li><li><strong>Use the Revert Check and Mutation Testing (</strong><strong>Stryker / </strong><strong>mutmut):</strong> As shown in 2026 research on <a href="https://doi.org/10.5753/cibse.2026.42441">mutation-guided LLM testing (<em>GEM</em>)</a> and <a href="https://www.augmentcode.com/guides/mutation-testing-ai-generated-code">Stryker MCP feedback loops</a>, never trust a green AI unit test until you verify it <strong>kills mutants</strong> (or manually break the core condition in the production diff and confirm the test fails). For incident debugging, restrict K8s/SQL agents to <strong>read-only evidence bundles</strong> (<a href="https://arxiv.org/pdf/2604.07830">Microsoft Research 2026</a>) that separate verified log facts from unverified hypotheses.</li></ul><p><strong>Stage 3 Mitigation (System Design &amp; RFCs: The 1-Page Anti-Workslop Brief &amp; </strong><strong>&lt;details&gt; Pattern):</strong></p><ul><li><strong>Enforce a 1-page RFC/ADR cap (</strong><strong>&lt;=350 words):</strong> Stop the 1 hour 56 minute workslop transfer tax (<a href="https://hbr.org/2025/09/ai-generated-workslop-is-destroying-productivity">Stanford &amp; BetterUp HBR study</a>) using the <a href="https://file+.vscode-resource.vscode-cdn.net/Users/tharindu/git/pgvector-filtering-investigation/ai-sprint-bottleneck/toolkit/skills/one-page-rfc/SKILL.md">One-Page RFC Skill</a>. Every design doc must open with four human-verified blocks: <em>(1) Problem &amp; Blast Radius in 3 sentences, (2) Explicit Non-Goals, (3) A Trade-Off Table comparing a minimal edit vs. a new abstraction, and (4) Labeled Unverified Assumptions</em>.</li><li><strong>Collapse raw appendices:</strong> If raw SQL plans, JSON schemas, or benchmark logs are needed for verification, wrap them in collapsible HTML &lt;details&gt;&lt;summary&gt;Appendix: Raw Evidence&lt;/summary&gt;...&lt;/details&gt; tags so reviewers see the big picture first.</li></ul><p><strong>Stage 4 Mitigation (Sprint Planning: </strong><strong>WIP = 1 and Executable Completion Evidence):</strong></p><ul><li><strong>Stop agent overreach with </strong><strong>WIP = 1:</strong> To prevent the +67.4% daily PR context-switching and +26% stalled-task surge measured by <a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025">Faros AI</a>, enforce <strong>Work-in-Progress = 1</strong> per agent thread (<a href="https://walkinglabs.github.io/learn-harness-engineering/en/lectures/lecture-07-why-agents-overreach-and-under-finish/">Learn Harness Engineering, 2026</a>). Every sprint ticket must specify an <strong>executable completion command</strong> (a failing test or curl/SQL assertion that turns green), not &quot;code is written.&quot;</li></ul><p><strong>Stage 5 Mitigation (PR Code Review: Stacked PRs </strong><strong>&lt;150 LOC &amp; Shift-Left IDE Audits):</strong></p><ul><li><strong>Decompose large AI diffs into Stacked PRs:</strong> Both <a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-the-2025-dora-report">Google Cloud DORA</a> and <a href="https://github.blog/engineering/turn-one-giant-ai-generated-pull-request-to-a-reviewable-stack/">GitHub Engineering</a> highlight <strong>Stacked Pull Requests</strong> (using GitHub’s native gh stack CLI or <a href="https://graphite.com/">Graphite</a>) as the antidote to 400+ LOC AI diffs sitting 17.6 hours for pickup. Split every feature into 2 to 3 dependent layers under 150 LOC each (Layer 1: types/schema; Layer 2: core logic + tests; Layer 3: endpoint wiring) so a human can review each layer in 10 minutes.</li><li><strong>Shift AI review left into the IDE:</strong> Instead of letting &lt;30%-signal PR bots slow down PR merges by +42% (<a href="https://arxiv.org/html/2604.03196">MSR 2026</a>; <a href="https://arxiv.org/html/2412.18531v2">Bilkent/Beko</a>), run high-confidence (&gt;=80/100) checks locally before git push using <a href="https://github.com/anthropics/claude-plugins-official/tree/main/plugins/code-review">/code-review</a> or the <a href="https://file+.vscode-resource.vscode-cdn.net/Users/tharindu/git/pgvector-filtering-investigation/ai-sprint-bottleneck/toolkit/skills/pre-push-review/SKILL.md">Pre-Push Review Skill</a>.</li></ul><p><strong>Stage 6 Mitigation (CI/CD &amp; Production Ops: Scoped Merge Queues &amp; Blast-Radius Budgets):</strong></p><ul><li><strong>Upgrade serial merge queues for </strong><strong>4× agent volume:</strong> In late 2026, <a href="https://github.blog/engineering/architecture-optimization/building-git-infrastructure-for-agent-scale-development/">GitHub Engineering reported a nearly </a><a href="https://github.blog/engineering/architecture-optimization/building-git-infrastructure-for-agent-scale-development/">4× surge in PR merges and GitHub Actions runs</a>, while <a href="https://www.anthropic.com/engineering/redesigning-source-control">Anthropic Engineering reported a </a><a href="https://www.anthropic.com/engineering/redesigning-source-control">10×+ surge in merge volume</a>. Human-scale single-lane merge queues choke under agent volume. Use <strong>batched, speculative, and monorepo-scoped merge queues</strong> (<a href="https://trunk.io/blog/merge-fast-or-merge-cheap-fine-tuning-merge-queues-to-handle-an-increase-in-prs-f">Trunk</a>, <a href="https://mergify.com/blog/merge-queues-and-ai-coding-agents">Mergify</a>) that test compatible PRs in batches and bisect only when a batch fails.</li><li><strong>Enforce a Blast-Radius Budget for AI-authored changes:</strong> Counteract the +242.7% production incident spike (<a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025">Faros AI 2026</a>) with <a href="https://developertoolkit.ai/en/workflows/ship/progressive-delivery/">Progressive Delivery guardrails</a>: put AI-generated logic behind feature flags with automated 5% -&gt; 25% -&gt; 100% canary rollbacks that trigger in seconds without a CI rebuild, and require expand-and-contract migrations for any database schema change.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ydYcU7LhIr8NKLd3uwiGTQ.png" /><figcaption><em>Figure 8: The stage-by-stage engineering mitigation architecture across all six stages of the 80-hour sprint, pairing each AI failure mode with its concrete CI gate, skill, or subagent guardrail.</em></figcaption></figure><h4>6.3 Ready-to-Use Toolkit: Drop-In CLAUDE.md, AGENTS.md, GEMINI.md, Cursor Rules &amp; Skills</h4><p>To make these guardrails immediately actionable, this repository includes a complete, copy-pasteable <a href="https://github.com/tharindu-sankalpa/ai-sprint-bottleneck/blob/main/toolkit/README.md"><strong>toolkit/</strong></a> directory that configures <strong>Claude Code, Cursor, OpenAI Codex CLI, and Google Gemini CLI</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7WbKSj-LiMxP7c8qB4XZtA.png" /></figure><p>Here is the core <strong>Response Shape &amp; Anti-Chat-Slop</strong> block from <a href="https://github.com/tharindu-sankalpa/ai-sprint-bottleneck/blob/main/toolkit/CLAUDE.md"><strong>toolkit/CLAUDE.md</strong></a> (and <a href="https://github.com/tharindu-sankalpa/ai-sprint-bottleneck/blob/main/toolkit/AGENTS.md"><strong>toolkit/AGENTS.md</strong></a>) that you can drop into any repository today:</p><pre>## Response Shape &amp; Anti-Chat-Slop (BLUF + Progressive Disclosure)<br><br>- **BLUF (Bottom Line Up Front):** Always open with a 1 to 2 sentence summary stating the root cause, architectural decision, or result. No preamble, no restating my prompt, and no closing &quot;next steps&quot; filler.<br>- **Max 3 to 5 bullets for the big picture:** Group explanations by system behavior and architecture, never by a line-by-line walkthrough of every file touched.<br>- **After editing files:** List only the modified file paths (`path/to/file.ext:line`), one per line, with a one-line reason. Do not narrate the diff in prose; I will read the diff in the editor.<br>- **Gate the nitty-gritty (Progressive Zoom):** Stop after giving the high-level mental map (Zoom Level 1). If deeper implementation or query-plan details exist, end with a single prompt: *&quot;Want to zoom into [A: data flow &amp; state transitions] or [B: line-by-line edge cases]?&quot;* Expand only when asked.<br>- **Delegate noisy searches to subagents:** When searching across &gt;3 files or inspecting long K8s/CI/SQL logs, use an isolated subagent and return only a 3-sentence summary + top 5 `file:line` entry points.</pre><h4>One-Command Global &amp; Project Installation (Claude Code, OpenAI Codex &amp; Cursor)</h4><p>Each skill in <a href="https://github.com/tharindu-sankalpa/ai-sprint-bottleneck/blob/main/toolkit/README.md"><strong>toolkit/skills/</strong> </a>includes both a standard SKILL.md (used by <strong>Claude Code</strong>, <strong>Cursor</strong>, and <strong>OpenAI Codex</strong>) and an agents/openai.yaml metadata manifest for <strong>Codex</strong> discovery. Run the commands below from the root of this repository to install the skills, subagents, and low-verbosity rules across all three tools:</p><p><strong>1. Install in Claude Code (Global </strong><strong>~/.claude/ + Project </strong><strong>.claude/):</strong></p><pre># Install global instructions, all 4 skills, and 2 isolated subagents:<br>mkdir -p ~/.claude/skills ~/.claude/agents<br>cp toolkit/CLAUDE.md ~/.claude/CLAUDE.md<br>cp -r toolkit/skills/* ~/.claude/skills/<br>cp toolkit/agents/*.md ~/.claude/agents/<br><br># Inside any Claude Code session, also enable the built-in concise output style:<br># /output-style concise</pre><ul><li><strong>How to invoke in Claude Code:</strong> Type /progressive-zoom, /debloat-audit, /one-page-rfc, or /pre-push-review in chat (or let Claude trigger them automatically based on your prompt).</li></ul><p><strong>2. Install in OpenAI Codex CLI (Global </strong><strong>~/.codex/ &amp; </strong><strong>~/.agents/):</strong></p><pre># Install global AGENTS.md instructions and all 4 skills (with agents/openai.yaml):<br>mkdir -p ~/.codex/skills ~/.agents/skills<br>cp toolkit/AGENTS.md ~/.codex/AGENTS.md<br>cp -r toolkit/skills/* ~/.codex/skills/<br>cp -r toolkit/skills/* ~/.agents/skills/<br><br># Add low-verbosity settings to ~/.codex/config.toml (or copy toolkit/codex-config.toml):<br># personality = &quot;pragmatic&quot;<br># model_verbosity = &quot;low&quot;<br># hide_agent_reasoning = true</pre><ul><li><strong>How to invoke in Codex:</strong> Reference any skill with $progressive-zoom, $debloat-audit, $one-page-rfc, or $pre-push-review (or run /skills inside codex to browse them).</li></ul><p><strong>3. Install in Cursor IDE (Global </strong><strong>~/.cursor/skills/ + Project </strong><strong>.cursor/rules/):</strong></p><pre># Install all 4 skills globally across all Cursor workspaces:<br>mkdir -p ~/.cursor/skills<br>cp -r toolkit/skills/* ~/.cursor/skills/<br><br># Install the always-on .mdc rule and AGENTS.md in your project repository:<br>mkdir -p .cursor/rules<br>cp toolkit/cursor-rules/anti-slop-and-bluf.mdc .cursor/rules/<br>cp toolkit/AGENTS.md ./AGENTS.md</pre><ul><li><strong>How to invoke in Cursor:</strong> Type /progressive-zoom, /debloat-audit, /one-page-rfc, or /pre-push-review in Cursor Agent chat, use /visualize for architecture diagrams, and press Shift+Tab for Plan Mode before multi-file edits.</li></ul><blockquote>For additional per-stage prompt templates and CI gate examples (jscpd, Stryker mutation checks, gh stack), see <a href="https://github.com/tharindu-sankalpa/ai-sprint-bottleneck/blob/main/toolkit/README.md"><strong>toolkit/README.md</strong></a> and <a href="https://github.com/tharindu-sankalpa/ai-sprint-bottleneck/blob/main/toolkit/templates/stage-by-stage-prompts.md"><strong>toolkit/templates/stage-by-stage-prompts.md</strong></a>.</blockquote><h3>7. Limitations: Where AI Does Deliver 5× to 10× Speedups</h3><p>To keep this analysis balanced, we must be clear about where the 80-hour bottleneck model applies and where it does not:</p><ul><li><strong>Greenfield Prototypes &amp; Zero-to-One MVPs:</strong> When a single engineer is building a brand-new prototype, an internal CLI tool, a benchmark harness, or a proof-of-concept with zero legacy callers, no multi-reviewer queue, and low blast radius, the downstream review tax is near zero. In those environments, 3× to 10× end-to-end speedups are real and routine.</li><li><strong>Self-Contained Mechanical Migrations:</strong> Well-scoped, repeatable transformations (such as upgrading an SDK across 50 files with an existing high-coverage integration test suite) bypass design ambiguity and human intent review.</li><li><strong>Production Multi-Engineer Systems (The 90% Case):</strong> Once a codebase has multiple contributors, shared state, customer SLAs, and compliance boundaries, <strong>the Pipeline Bottleneck Rule governs everything</strong>.</li></ul><h3>8. Conclusion: The Question to Ask Next Sprint</h3><p>AI did not fail to speed up software engineering. It succeeded so dramatically at speeding up <strong>drafting and generation</strong> (code, SQL queries, test files, RFC drafts, meeting notes, and review comments) that it overloaded the one resource in the sprint that cannot be parallelized by tokens: <strong>human understanding and accountability</strong>.</p><p>The next time a vendor, executive, or benchmark claims a new AI model will make your engineering team 10× faster, do not argue about whether the model can write code 10× faster.</p><p>Show them the 80-hour sprint breakdown, point to the 35.5 hours of cleanup, debugging, and review tax, and ask:</p><blockquote><strong><em>“It makes generating code and docs 10× faster. How fast can our team review, verify, and debug what it just generated?”</em></strong></blockquote><h3>Repository Layout</h3><pre>README.md                         Canonical Medium article and 80-hour sprint analysis<br>docs/claude-design-brief.md       13-page LinkedIn PDF carousel brief for Claude Design + LinkedIn post copy<br>docs/nano-banana-prompts.md       13 image generation prompts (9 in 16:9 for article + 4 in 3:4 for carousel)<br>docs/diagrams/                    Generated 2K diagram images (01..09 for article, li-01..li-04 for carousel)<br>toolkit/                          Drop-in CLAUDE.md, AGENTS.md, GEMINI.md, Cursor rules, skills, subagents &amp; prompts</pre><h3>Further Reading &amp; Linked Sources</h3><ul><li><a href="https://243608892.fs1.hubspotusercontent-na2.net/hubfs/243608892/AI_Engineering_Impact_Report_July_2025_Faros_AI.pdf">Faros AI (July 2025): The AI Productivity Paradox Report (10,000+ developers across 1,255 teams)</a></li><li><a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025">Faros AI (2026): The Acceleration Whiplash &amp; DORA 2025 Key Takeaways (22,000 developers across 4,000+ teams)</a></li><li><a href="https://assets.linearb.io/image/upload/v1765300694/resources/LinearB_2026_Software_Engineering_Benchmarks_Report.pdf">LinearB (2026): Software Engineering Benchmarks Report, The AI Productivity Edition (8.1M+ pull requests across 4,813 teams)</a></li><li><a href="https://www.gitclear.com/the_ai_code_quality_maintainability_gap">GitClear (2026): The Maintainability Gap, AI Code Quality Research (623M changed lines, 2023 to 2026)</a> and <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">GitClear (2025): AI Copilot Code Quality Research (211M changed lines)</a></li><li><a href="https://arxiv.org/pdf/2604.07830">Oregon State University &amp; Microsoft Research (2026): To Copilot and Beyond, 22 AI Systems Developers Want Built (n=860 Microsoft engineers)</a></li><li><a href="https://hbr.org/2025/09/ai-generated-workslop-is-destroying-productivity">Harvard Business Review, Stanford University &amp; BetterUp Labs (Sept 2025): AI-Generated “Workslop” Is Destroying Productivity</a> and <a href="https://www.betterup.com/hubfs/Post-Uplift%20content/BetterUp_Uplift_The%20hidden%20cost%20of%20workslop.pdf">Full BetterUp × Stanford Report PDF</a></li><li><a href="https://arxiv.org/html/2604.03196">MSR 2026 Study: From Industry Claims to Empirical Reality, Code Review Agents in Pull Requests (3,109 PRs)</a> and <a href="https://dl.acm.org/doi/abs/10.1145/3793302.3793599">MSR 2026 Companion Study on Reviewer Bot Footprints (4,532 PRs)</a></li><li><a href="https://arxiv.org/html/2412.18531v2">Bilkent University &amp; Beko (2025): Automated Code Review In Practice (238 practitioners, 4,335 PRs)</a></li><li><a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">METR Randomized Controlled Trial (July 2025): Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity</a> and <a href="https://metr.org/blog/2026-02-24-uplift-update/">METR February 2026 Update</a></li><li><a href="https://dora.dev/insights/balancing-ai-tensions/">Google Cloud DORA (2025): Balancing AI Tensions Across the SDLC</a> and <a href="https://dora.dev/insights/customization-supports-developer-engagement/">DORA / UC Berkeley Study on Customization &amp; Developer Engagement</a></li><li><a href="https://www.atlassian.com/blog/bitbucket/devex-bitbucket-pipelines-versus-jenkins">Atlassian State of DevEx Report (2025)</a> and <a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2024/11/Time-Warp-Developer-Productivity-Study.pdf">Microsoft Research Time Warp Developer Productivity Study</a></li><li><a href="https://www.sonarsource.com/blog/how-much-time-do-developers-spend-actually-writing-code/">Pre-AI Baseline Studies: Sonar Developer Survey (2019, 32% coding)</a>, <a href="https://stripe.com/files/reports/the-developer-coefficient.pdf">Stripe “The Developer Coefficient” Report</a>, and <a href="https://resources.garden.io/hubfs/Garden_Developer_Productivity_Survey_Report_2021.pdf">Garden.io Developer Productivity Report (2021)</a></li><li><a href="https://leaddev.com/wp-content/uploads/2025/08/THE-AI-IMPACT-REPORT-2025-download__LDMO__.pdf">LeadDev (2025): The AI Impact Report (n=883 engineering leaders &amp; developers)</a>, <a href="https://www.challengergray.com/wp-content/uploads/2026/01/Challenger-Report-December-2025.pdf">Challenger, Gray &amp; Christmas Job Cut Reports (2025/2026)</a>, and <a href="https://ailayoffs.org/research/counting-ai-layoffs-2026-10.pdf">Counting AI Layoffs Register (2023 to 2026, 243 layoff events)</a></li><li><a href="https://claude.com/resources/articles/steering-claude-code-skills-hooks-rules-subagents-and-more">Stage-by-Stage Engineering Mitigations: Anthropic Steering Claude Code (2026)</a>, <a href="https://github.com/anthropics/claude-plugins-official">Claude Code Official Plugins (</a><a href="https://github.com/anthropics/claude-plugins-official">pr-review-toolkit, </a><a href="https://github.com/anthropics/claude-plugins-official">feature-dev, </a><a href="https://github.com/anthropics/claude-plugins-official">code-simplifier)</a>, <a href="https://arxiv.org/html/2610.13065">ETH Zurich TestSuiteBench (2026)</a>, <a href="https://doi.org/10.5753/cibse.2026.42441">GEM Mutation-Guided LLM Testing (2026)</a>, <a href="https://github.blog/engineering/turn-one-giant-ai-generated-pull-request-to-a-reviewable-stack/">GitHub Stacked PRs (2026)</a>, <a href="https://jscpd.dev/guides/ci">jscpd CI Clone Gate</a>, <a href="https://github.blog/engineering/architecture-optimization/building-git-infrastructure-for-agent-scale-development/">GitHub Agent-Scale Git Infrastructure (2026)</a>, and <a href="https://www.anthropic.com/engineering/redesigning-source-control">Anthropic Redesigning Source Control (2026)</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7f3ffce5a10e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A multi-agent system for SRE workflows on Amazon EKS with kagent and kmcp]]></title>
            <link>https://medium.com/@wltsankalpa/a-multi-agent-system-for-sre-workflows-on-amazon-eks-with-kagent-and-kmcp-1d427b96e2ea?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/1d427b96e2ea</guid>
            <category><![CDATA[agentic-workflow]]></category>
            <category><![CDATA[multiagent-orchestration]]></category>
            <category><![CDATA[multi-agent-systems]]></category>
            <category><![CDATA[model-context-protocol]]></category>
            <category><![CDATA[a2a-protocol]]></category>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Mon, 19 Oct 2026 11:55:28 GMT</pubDate>
            <atom:updated>2026-10-19T11:55:28.398Z</atom:updated>
            <content:encoded><![CDATA[<p>I built and tested a multi-agent system for SRE workflows on Amazon EKS using kagent and kmcp. An orchestrator uses specialist agents as tools to diagnose Kubernetes faults and coordinate repairs. Agents can pause to ask the user for missing information or clarify the intended repair. Before a change runs, the operator’s approval gate also pauses execution and surfaces the proposed tool call to the user — even when another agent called the operator.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HKqECSbgY9JGIklRWLLm6g.jpeg" /><figcaption><em>The system. An SRE reaches the orchestrator through the kagent controller over A2A; the orchestrator delegates to six specialists, which use selected tools on five MCP servers; the tools act on the demo namespace and the monitoring stack. kmcp, PostgreSQL and kagent are the platform underneath.</em></figcaption></figure><p>Six of the seven agents in this multi-agent system are declarative: their model reference, system instructions and allowed tool calls are defined with an Agent custom resource in YAML. kagent deploys them using its runtime built on Google&#39;s Agent Development Kit (ADK). kagent also supports bring-your-own (BYO) agents: custom containers that serve the Agent-to-Agent (A2A) protocol. The seventh agent, the incident reporter, follows this path. I wrote its Python ReAct tool-calling loop myself rather than using a framework&#39;s built-in loop; the A2A Python SDK handles the server interface.</p><p>The kagent Helm installation comes with built-in Model Context Protocol (MCP) tool servers for Kubernetes, Helm, Prometheus, Grafana, Argo Rollouts and other operational systems. We can deploy existing MCP server packages for Node.js or Python, such as the documentation-fetch server, through kmcp by defining an MCPServer custom resource in YAML. kmcp can also deploy MCP servers from custom container images. My BYO network-probe server is built with FastMCP and deployed through kmcp. For declarative agents, YAML defines the agent&#39;s behaviour for kagent&#39;s runtime. For MCP servers, YAML tells kmcp how to run a server whose tools are already implemented in the package or container image.</p><p>The test environment was one EKS 1.34 cluster in ap-south-1, with two t3.xlarge nodes and five deliberately broken workloads. The troubleshooter diagnosed all five individual faults. The orchestrator repaired a Service through two human decisions and verified three endpoints. Its longest diagnostic request never completed: testing exposed a controller timeout, overlapping abandoned requests and a memory failure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Yd7sVaM8fC29HJ6MqKeDQw.png" /><figcaption><em>The EKS test cluster in </em><em>ap-south-1: two </em><em>t3.xlarge On-Demand nodes, both Ready.</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oTJbzeC2BT-l6dB56JB1PQ.png" /><figcaption><em>The demo workloads in k9s during a later AKS run: image-pull failures, a pending pod and restarting containers alongside two healthy </em><em>web pods. This screenshot illustrates the workloads; the test results reported here are from EKS.</em></figcaption></figure><p>The repository contains the manifests, custom code, API collection, raw responses and original build record. The framework provides the runtime and protocol integration; this project defines the specialist roles, adds custom capabilities and checks the resulting behavior against cluster evidence.</p><p>Repository: <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents">https://github.com/tharindu-sankalpa/eks-kagent-sre-agents</a></p><p>For request fields, tool inventories and transport details, see the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/runtime-and-protocols.md">runtime and protocol reference</a>. For copyable deployment commands, use the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/setup-guide.md">setup guide</a>. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/build-record.md">build record</a> retains the complete investigation and outputs.</p><h3>Delegation and approval in one repair</h3><p>The orchestrator has no direct Kubernetes tools. Its tools are six specialist agents, registered with type: Agent. Each specialist keeps its own model, instructions and allowed tools. kagent makes the specialist callable through A2A and returns its answer to the orchestrator as a tool result.</p><p>Scenario 15 asked:</p><blockquote><em>Fix the Service checkout in namespace demo-broken so that it has endpoints. Tell me what you intend to change before you do it.</em></blockquote><p>The request exercised the complete workflow:</p><ol><li>Diagnose. The orchestrator called cluster_troubleshooter. It found that the Service selected app: checkout, while the running backend pods were labelled app: web.</li><li>Resolve intent. Matching web looked plausible, but the intended backend was an inference. The orchestrator used ask_user to offer three choices, with the proposed patch and rollback. The task paused in input-required.</li><li>Delegate the change. The user’s answer resumed the same task. The orchestrator called sre_operator with the specific repair request.</li><li>Approve execution. The operator selected k8s_patch_resource, which is listed under its requireApproval. The task paused again. kagent surfaced the specialist&#39;s pending tool call and arguments through the parent conversation.</li><li>Apply and verify. After approval, the operator changed the selector to app: web. The Service had three endpoints, independently confirmed with kubectl. The final report included evidence, the change, rollback and specialists used.</li></ol><p>The three client exchanges took 32 s, 16 s and 14 s: 62 s of request time in total, with two human decisions and one change. Those timings do not measure human deliberation time.</p><p>The two pauses serve different purposes. The first asks which change the user intends. The second is the runtime gate on executing the selected tool. The gate is configured once on the operator and remains active when the operator is called by another agent.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OnpgUcOTN-PlqLiVwbQ7_w.png" /><figcaption><em>A separate UI run of the same repair request. The troubleshooter has completed; the orchestrator is asking the user to confirm the backend, with the patch and rollback visible.</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CZB-wvB-i_R5W-MBlkGb7w.jpeg" /><figcaption><em>Scenario 15 as a logical sequence: twelve messages, two human pauses and one patch. The controller hop and the operator’s individual MCP reads are omitted; the operator’s pending patch runs only after the second decision.</em></figcaption></figure><p>The direct operator tests checked execution approval separately. Approving a scale changed web from two to three replicas. Rejecting the opposite change left it at three. The replica counts were checked before and after; the rejected tool call did not execute.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LaEipGNgtQ0ZNFcMykGGGA.png" /><figcaption><em>A later Langfuse trace from 2026–10–18 shows the </em><em>ask_user exchange: the backend-selection question, four choices and the recorded answer to repoint </em><em>checkout to </em><em>app=web. The trace tree also shows delegation to </em><em>sre_operator and its Kubernetes tool calls. These choices and timings belong to this later run, rather than the original EKS scenario 15 measurements.</em></figcaption></figure><h3>What the frameworks provide</h3><h4>kagent: configuration, runtime and built-in tools</h4><p>kagent brings agent configuration into Kubernetes custom resources. In this build, a ModelConfig selects the provider and model, an Agent specifies the instructions and tools, and a RemoteMCPServer identifies an existing tool endpoint. Applying an Agent creates its Deployment and Service.</p><p>The chart supplies the operational tools. Its main kagent-tools server exposed 124 tools in this run, including Kubernetes resource inspection, events, logs, scaling and patching; Helm operations; and Prometheus queries. The chart also provides a Grafana MCP server and documentation search. A troubleshooter can therefore use existing tools without implementing Kubernetes API wrappers.</p><p>The controller and the agent runtime have separate jobs. The controller reconciles Kubernetes resources and deploys agents. Inside each declarative Python agent, kagent builds on Google ADK to run the conversation and tool execution. kagent adds its MCP integration, A2A serving, session handling and approval behavior. The pinned v0.9.10 source imports ADK’s Runner and uses ToolContext.request_confirmation() for approval. <a href="https://github.com/kagent-dev/kagent/blob/v0.9.10/python/packages/kagent-adk/src/kagent/adk/_agent_executor.py">Runtime source</a>, <a href="https://github.com/kagent-dev/kagent/blob/v0.9.10/python/packages/kagent-adk/src/kagent/adk/_approval.py">approval source</a>.</p><p>All six declarative agents ultimately ran on Python. The Go runtime was attempted, but the chart’s pinned image digest was unavailable. The custom seventh agent uses its own loop and the A2A SDK; it does not use Google ADK.</p><p>The Agent resource has two types. type: Declarative is the YAML shown below: the controller creates the Deployment, and the pod runs kagent&#39;s runtime, built on Google ADK; the runtime field selects the Python ADK (the default) or the Go ADK. type: BYO replaces the model, instructions and tools with a container image, and the only contract is that the image serves A2A on port 8080. Any framework that speaks A2A fits: kagent&#39;s examples use Google ADK, LangGraph and CrewAI, and this build uses a plain Python loop. The two types mix freely. Here the five specialists that need only tools and instructions, and the orchestrator, are declarative; the incident reporter, which needs its own control flow, is BYO. Both appear to the orchestrator as type: Agent tools. <a href="https://github.com/kagent-dev/kagent/blob/v0.9.10/go/api/v1alpha2/agent_types.go">Resource definition</a>, <a href="https://kagent.dev/docs/kagent/examples/">BYO examples</a>.</p><h4>kmcp: running tool servers on Kubernetes</h4><p>kmcp manages MCP servers rather than agent conversations. Its CLI scaffolds, builds and deploys projects. Its controller turns an MCPServer resource into a Deployment, Service, ConfigMap and ServiceAccount.</p><p>This build uses two deployment paths:</p><ul><li>An existing stdio package: fetch reads documentation pages. kmcp starts the package through agentgateway, which adapts its stdin/stdout interface to MCP over HTTP. No custom image was built.</li><li>A custom HTTP server: netprobe-mcp serves four network checks through FastMCP. kmcp init supplied the scaffold, kmcp build built and pushed the image, and an MCPServer resource deployed it without a stdio adapter.</li></ul><p>kmcp ships the deployment machinery; kagent’s chart ships the built-in operational tool servers. <a href="https://github.com/kagent-dev/kmcp/tree/v0.3.0">kmcp source</a>.</p><h4>The two protocol boundaries</h4><p>Model Context Protocol (MCP) lets an agent discover and call tools. Agent-to-Agent (A2A) lets a client or another agent discover and invoke an agent, receive progress and continue paused tasks.</p><p>For a repair, the path is: user → orchestrator over A2A → specialist over A2A → tool server over MCP → Kubernetes API. Model calls go to OpenRouter. Using the same invocation interface for declarative and custom agents lets the orchestrator call both without owning their internal loops.</p><p>These are established framework capabilities. The engineering work here is composing them into an SRE workflow, constraining the available operations, implementing the custom pieces and testing what holds under delegation.</p><h3>Defining an agent in YAML</h3><p>A declarative agent has three core inputs, plus execution approval where needed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-segFH5YVzhs1y9DpXVI_Q.png" /></figure><p>The following is a shortened example based on <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/infra/kagent/agents/sre-operator.yaml">sre-operator.yaml</a>, with the instructions condensed and the tool list abbreviated. The full file includes all eleven allowed tools and all six approval-gated operations.</p><pre>spec:<br>  type: Declarative<br>  declarative:<br>    modelConfig: grok-4-7<br>    systemMessage: |<br>      Diagnose before you act. State the root cause and proposed change.<br>      After a change, verify and report the before and after state.<br>    tools:<br>      - type: McpServer<br>        mcpServer:<br>          apiGroup: kagent.dev<br>          kind: RemoteMCPServer<br>          name: kagent-tool-server<br>          toolNames: [k8s_get_resources, k8s_patch_resource]<br>          requireApproval: [k8s_patch_resource]</pre><p>toolNames limits the tool catalogue exposed to the model. requireApproval is enforced by the runtime before execution; it is not merely a prompt instruction. Neither field replaces Kubernetes RBAC, authentication or network policy. This lab uses the chart&#39;s default Kubernetes permissions.</p><p>The orchestrator selects agents instead of MCP tools:</p><pre>tools:<br>  - type: Agent<br>    agent: { name: cluster-troubleshooter }<br>  - type: Agent<br>    agent: { name: sre-operator }</pre><p>The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/infra/kagent/agents/sre-orchestrator.yaml">full orchestrator</a> lists all six specialists. Its instructions live in a ConfigMap referenced by systemMessageFrom. promptTemplate includes the built-in safety snippet through {{include &quot;builtin/safety-guardrails&quot;}}; other agents use the Kubernetes context snippet. Prompts and tool lists remain reviewable in Git.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-H9LTXjsa78g4kgQ2l0w5w.jpeg" /><figcaption><em>The four fields of </em><em>sre-operator that decide its behaviour. The excerpts are selected lines, not a complete manifest; the approval gate covers six of its eleven tools.</em></figcaption></figure><h3>Specialists and custom implementations</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RPYxz7ocB9S5yr5yo9HjwA.png" /></figure><p>Five ModelConfig resources share one OpenRouter Secret. They use provider: OpenAI with an OpenRouter baseUrl; the model identifier differs. This makes model selection independent of agent deployment, but this experiment did not compare models on the same task.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pOK50Heo7hX7ZtTnq_5YTw.png" /><figcaption><em>The provider reads </em><em>OpenAI for every entry because each model is reached through OpenRouter&#39;s OpenAI-compatible API.</em></figcaption></figure><h3>Custom MCP tools: checking the network from inside the cluster</h3><p>Kubernetes state can explain a bad selector, but an SRE also needs to test whether a name resolves and a Service actually answers. I added dns_lookup, tcp_connect, http_check and tls_cert_expiry as four typed Python functions. FastMCP derives their tool schemas from type hints and docstrings.</p><p>The server uses httpx for HTTP and certifi for a consistent TLS trust bundle. Its image was 98 MB. kmcp supplied the scaffold and build path; the probe functions are the custom implementation. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/byo/netprobe-mcp/README.md">component README</a> documents inputs, outputs, local execution and extension.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mUfaF-YALcnABPTrJCkNlQ.jpeg" /><figcaption><em>Each probe has its own Python module. The loader imports those modules, their decorators register tools on the shared FastMCP instance, and clients discover the definitions with </em><em>tools/list. Input schemas declare arguments; the current output schemas declare only an object, while the cards describe the fields returned by the functions.</em></figcaption></figure><h4>Deploy the custom MCP server with YAML</h4><p>After building and pushing the image, an MCPServer resource tells kmcp how to run it. This is the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/infra/kmcp/netprobe-mcpserver.yaml">netprobe manifest</a>, with explanatory comments omitted. Replace &lt;ACCOUNT_ID&gt; with the ECR account used for the build; the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/setup-guide.md#8-build-and-deploy-the-custom-mcp-server">setup guide</a> contains the build, push and apply commands.</p><pre>apiVersion: kagent.dev/v1alpha1<br>kind: MCPServer<br>metadata:<br>  name: netprobe-mcp<br>  namespace: kagent<br>spec:<br>  transportType: http<br>  httpTransport:<br>    targetPort: 3000<br>    path: /mcp<br>  deployment:<br>    image: &lt;ACCOUNT_ID&gt;.dkr.ecr.ap-south-1.amazonaws.com/netprobe-mcp:0.1.0<br>    port: 3000<br>    cmd: python<br>    args:<br>      - src/main.py<br>      - --transport<br>      - http<br>      - --host<br>      - 0.0.0.0<br>      - --port<br>      - &quot;3000&quot;</pre><p>The tool functions and their schemas live in the image; the YAML selects that image, startup command, transport and port. kmcp creates the Deployment and Service. FastMCP already serves HTTP at /mcp, so this deployment needs no stdio-to-HTTP adapter. Agents reach it at http://netprobe-mcp.kagent:3000/mcp. These deployment settings do not define a model or an agent loop.</p><h4>Try the MCP server without a container</h4><p>The probe functions and MCP interface can run independently of Kubernetes, kmcp and any language model. From the repository root, start the Python server and leave it running:</p><pre>cd byo/netprobe-mcp<br>uv sync<br>uv run python src/main.py --transport http --host 127.0.0.1 --port 3000</pre><p>In another terminal, initialize the MCP connection. Save the response headers separately so they do not get passed to jq:</p><pre>MCP_URL=&#39;http://127.0.0.1:3000/mcp&#39;<br>MCP_HEADERS=$(mktemp)<br>MCP_INIT=$(mktemp)<br><br>curl -sSN &quot;$MCP_URL&quot; -D &quot;$MCP_HEADERS&quot; \<br>  -H &#39;Content-Type: application/json&#39; \<br>  -H &#39;Accept: application/json, text/event-stream&#39; \<br>  --data &#39;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;initialize&quot;,&quot;params&quot;:{&quot;protocolVersion&quot;:&quot;2025-03-26&quot;,&quot;capabilities&quot;:{},&quot;clientInfo&quot;:{&quot;name&quot;:&quot;article-curl&quot;,&quot;version&quot;:&quot;1.0&quot;}}}&#39; |<br>sed -E &#39;/^(event:|id:|retry:|:)/d; s/^data: *//&#39; | jq . | tee &quot;$MCP_INIT&quot;<br>MCP_SESSION_ID=$(awk &#39;tolower($1) == &quot;mcp-session-id:&quot; {sub(/\r$/, &quot;&quot;, $2); print $2}&#39; &quot;$MCP_HEADERS&quot;)<br>MCP_PROTOCOL_VERSION=$(jq -r &#39;.result.protocolVersion&#39; &quot;$MCP_INIT&quot;)</pre><p>Check that initialization succeeded and returned the requested protocol version before continuing. Mcp-Session-Id comes from the server; the JSON-RPC id identifies one request. The response can be JSON or Server-Sent Events (SSE), with JSON inside data: lines. The sed step removes that framing for jq; it handles the single-line events produced here, rather than every possible SSE format.</p><p>Next, send the initialized notification, then discover the tools and their schemas:</p><pre>curl -sSN &quot;$MCP_URL&quot; \<br>  -H &#39;Content-Type: application/json&#39; \<br>  -H &#39;Accept: application/json, text/event-stream&#39; \<br>  -H &quot;Mcp-Session-Id: $MCP_SESSION_ID&quot; \<br>  -H &quot;MCP-Protocol-Version: $MCP_PROTOCOL_VERSION&quot; \<br>  --data &#39;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;notifications/initialized&quot;}&#39; |<br>sed -E &#39;/^(event:|id:|retry:|:)/d; s/^data: *//&#39; | jq .<br><br>curl -sSN &quot;$MCP_URL&quot; \<br>  -H &#39;Content-Type: application/json&#39; \<br>  -H &#39;Accept: application/json, text/event-stream&#39; \<br>  -H &quot;Mcp-Session-Id: $MCP_SESSION_ID&quot; \<br>  -H &quot;MCP-Protocol-Version: $MCP_PROTOCOL_VERSION&quot; \<br>  --data &#39;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:2,&quot;method&quot;:&quot;tools/list&quot;}&#39; |<br>sed -E &#39;/^(event:|id:|retry:|:)/d; s/^data: *//&#39; | jq .</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4qhdGUn0SbUxwNn1-dwwXg.png" /><figcaption><em>The local walkthrough on 2026–10–19. The left terminal shows all four tools loading and the HTTP server starting; the right shows the initialization response, the notification and tool discovery. The startup log records HTTP 202 for the notification.</em></figcaption></figure><p>The notification has no request id and normally returns HTTP 202 with an empty body, so jq prints nothing. tools/list should return the four probe definitions. Now call one:</p><pre>curl -sSN &quot;$MCP_URL&quot; \<br>  -H &#39;Content-Type: application/json&#39; \<br>  -H &#39;Accept: application/json, text/event-stream&#39; \<br>  -H &quot;Mcp-Session-Id: $MCP_SESSION_ID&quot; \<br>  -H &quot;MCP-Protocol-Version: $MCP_PROTOCOL_VERSION&quot; \<br>  --data &#39;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:3,&quot;method&quot;:&quot;tools/call&quot;,&quot;params&quot;:{&quot;name&quot;:&quot;dns_lookup&quot;,&quot;arguments&quot;:{&quot;hostname&quot;:&quot;example.com&quot;}}}&#39; |<br>sed -E &#39;/^(event:|id:|retry:|:)/d; s/^data: *//&#39; | jq .<br><br>rm &quot;$MCP_HEADERS&quot; &quot;$MCP_INIT&quot;</pre><p>Inspect result.structuredContent.resolved and addresses; the actual addresses depend on DNS at the time. The function runs on the laptop here. Calling a port-forwarded netprobe server instead runs the same check from its pod, where Kubernetes Service names and connectivity have meaning. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/byo/netprobe-mcp/README.md#call-the-mcp-server-manually-with-curl-and-jq">MCP README walkthrough</a> continues with TCP, HTTP, TLS, failed DNS and protocol/tool errors, following the <a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle">MCP initialization lifecycle</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5ItNLVnYZRZAxoQGuQbXhg.png" /><figcaption><em>The local DNS call returns request </em><em>id: 3, </em><em>resolved: true and </em><em>isError: false. The same observation appears as JSON text in </em><em>content and as an object in </em><em>structuredContent; the addresses are specific to this lookup.</em></figcaption></figure><p>For comparison, these selected fields come from the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/raw/netprobe-call-dns_lookup.json">recorded in-cluster DNS response</a>, which checked checkout rather than the local example&#39;s public hostname:</p><pre>{<br>  &quot;result&quot;: {<br>    &quot;structuredContent&quot;: {<br>      &quot;resolved&quot;: true,<br>      &quot;addresses&quot;: [&quot;10.100.237.153&quot;],<br>      &quot;canonical_name&quot;: &quot;checkout.demo-broken.svc.cluster.local&quot;<br>    },<br>    &quot;isError&quot;: false<br>  }<br>}</pre><p>DNS resolved even though the Service had no endpoints. The separate <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/raw/netprobe-call-tcp_connect.json">TCP response</a> records the connection failure; the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/raw/netprobe-call-http_check.json">HTTP response</a> checks the kagent UI, and the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/raw/netprobe-call-tls_cert_expiry.json">TLS response</a> checks a public certificate. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/raw/kagent-tools-tools-list.json">built-in tool catalogue</a> contains the operational tool definitions and schemas. These are recorded responses, not outputs readers need to reproduce byte for byte.</p><p>For Postman, import the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/postman/kagent.postman_collection.json">existing collection</a>, set netprobe_url to http://127.0.0.1:3000 and open 4b · Call the bring-your-own MCP server directly. Send initialize, the initialized notification, tools/list, then the four tool calls. Initialization saves netprobe_session_id. Inspect the raw bodies and use public targets for a local server; the original Service examples expect an in-cluster server. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/byo/netprobe-mcp/README.md#use-the-postman-collection">README&#39;s Postman section</a> explains the variables and response checks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UxLxADhFdupbvsXyl2QOsw.png" /><figcaption><em>Two </em><em>RemoteMCPServer resources offered 124 and 65 tools; two kmcp-managed </em><em>MCPServer resources offered one and four; a labelled Service offered documentation search. Total: 195 tools. Each agent sees only its selected subset.</em></figcaption></figure><h3>Custom agent: owning the tool-calling loop</h3><p>The incident reporter is about 230 lines across <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/byo/incident-reporter-agent/src/agent.py">agent.py</a> and <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/byo/incident-reporter-agent/src/server.py">server.py</a>. It connects to the built-in tool server and netprobe, discovers their schemas, filters them to six Kubernetes tools and four probes, and sends those definitions with the conversation to the model.</p><p>When the model requests a tool, the loop routes the call to the server that owns it, appends the result and asks the model again. It stops on a text answer or the configured turn limit. The implementation defaults to 20 turns and truncates each tool result to 12,000 characters.</p><p>The A2A SDK wrapper emits working updates per tool step, publishes the report as an artifact and ends in completed or failed. kagent requires port 8080, an agent card at /.well-known/agent-card.json and A2A 0.3 JSON-RPC at /. The card is also the readiness endpoint. The controller publishes its own card and injects KAGENT_NAME, KAGENT_NAMESPACE and KAGENT_URL.</p><p>The image was 57 MB and its pod became Ready in 19 s. The report scenario diagnosed all five faults in 161 s with 21 tool calls, including DNS, TCP and HTTP probes of checkout. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/runtime-and-protocols.md#the-custom-agent-boundary">protocol reference</a> covers the request loop and container contract in detail.</p><h4>Deploy the BYO agent with YAML</h4><p>The reporter uses kagent’s Agent resource with type: BYO. This is its <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/infra/kagent/agents/incident-reporter.yaml">deployment manifest</a>, with header comments omitted. The image must already be pushed, the referenced Secret must exist, and both MCP endpoints must be reachable. Replace &lt;ACCOUNT_ID&gt; with the build&#39;s ECR account.</p><pre>apiVersion: kagent.dev/v1alpha2<br>kind: Agent<br>metadata:<br>  name: incident-reporter<br>  namespace: kagent<br>spec:<br>  description: Bring-your-own agent. Writes an incident report (summary, evidence, root cause, fix) for a namespace, using the built-in tool server and the netprobe MCP server directly.<br>  type: BYO<br>  byo:<br>    deployment:<br>      image: &lt;ACCOUNT_ID&gt;.dkr.ecr.ap-south-1.amazonaws.com/incident-reporter-agent:0.1.0<br>      env:<br>        - name: OPENROUTER_API_KEY<br>          valueFrom:<br>            secretKeyRef:<br>              name: kagent-openrouter<br>              key: OPENROUTER_API_KEY<br>        - name: MODEL<br>          value: deepseek/deepseek-v4.1-flash<br>        - name: KAGENT_TOOLS_URL<br>          value: http://kagent-tools.kagent:8084/mcp<br>        - name: NETPROBE_URL<br>          value: http://netprobe-mcp.kagent:3000/mcp<br>      resources:<br>        requests: { cpu: 100m, memory: 256Mi }<br>        limits: { memory: 512Mi }</pre><p>Here the model is an environment variable and the key comes from a Secret reference; there is no spec.declarative.modelConfig, systemMessage or YAML tool list. The Python code owns the prompt, tool selection and ReAct loop. kagent runs the container and integrates its A2A interface, while the Dockerfile starts src/server.py on port 8080. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/setup-guide.md#9-build-the-custom-agent-image">setup guide</a> covers the image build, and <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/setup-guide.md#10-apply-model-prompt-and-agent-resources">agent deployment</a> covers applying the resources.</p><h4>Invoke the deployed agent directly</h4><p>The reporter needs model credentials and both MCP servers before it starts serving. To explore its interface, use the deployed instance: keep this port-forward running in a terminal pointed at the intended cluster.</p><pre>kubectl -n kagent port-forward svc/incident-reporter 8091:8080</pre><p>In another terminal, read the agent card and send the report request:</p><pre>AGENT_URL=&#39;http://127.0.0.1:8091&#39;<br>curl -sS &quot;$AGENT_URL/.well-known/agent-card.json&quot; | jq .<br><br>curl -sS --max-time 600 &quot;$AGENT_URL/&quot; \<br>  -H &#39;Content-Type: application/json&#39; \<br>  --data &quot;$(jq -n --arg message_id &quot;$(uuidgen)&quot; &#39;{<br>    jsonrpc:&quot;2.0&quot;, id:&quot;report-1&quot;, method:&quot;message/send&quot;,<br>    params:{message:{role:&quot;user&quot;,kind:&quot;message&quot;,messageId:$message_id,<br>      parts:[{kind:&quot;text&quot;,text:&quot;Write an incident report for namespace demo-broken.&quot;}]}}<br>  }&#39;)&quot; | jq .</pre><p>This is A2A at /, not MCP at /mcp: there is no MCP initialize/list/call handshake on this endpoint. The reporter&#39;s own MCP clients do that work when it starts. The A2A response carries the task state and the report in result.artifacts[].parts[].text. Changing the method to message/stream exposes the reporter&#39;s tool-step updates as SSE; the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/byo/incident-reporter-agent/README.md#watch-tool-steps-with-messagestream">agent README</a> gives the complete curl/jq example. Each request invokes the model and can incur provider charges.</p><p>The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/answers/incident-reporter/s12.json">original s12 response</a> includes these selected fields, with the report text omitted here:</p><pre>{<br>  &quot;result&quot;: {<br>    &quot;kind&quot;: &quot;task&quot;,<br>    &quot;status&quot;: {&quot;state&quot;: &quot;completed&quot;},<br>    &quot;artifacts&quot;: [{&quot;name&quot;: &quot;incident-report&quot;}]<br>  }<br>}</pre><p>The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/answers/incident-reporter/s12.txt">readable report</a> contains the diagnoses, quoted evidence and proposed fixes. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/summary/scenarios.json">scenario summary JSON</a> records task states and tool counts across the scenarios; the Results section below adds the accuracy verdicts, which task completion alone cannot establish.</p><p>The existing Postman collection invokes agents through the controller. Port-forward svc/kagent-controller on 8083:8083, set base_url=http://127.0.0.1:8083, ns=kagent and agent=incident-reporter, then use 6 · Invoke an agent (A2A). Duplicate its generic message/send request and replace the question with the report request above. To use the direct port-forward instead, change the duplicated request&#39;s URL to http://127.0.0.1:8091/; changing base_url alone would leave the controller&#39;s /api/a2a/... path attached. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/byo/incident-reporter-agent/README.md">agent README</a> explains the code, configuration, task states and both invocation paths.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eBJ5y326_7uw_bbxyfVcjg.jpeg" /></figure><p><em>The custom agent. server.py receives </em><em>message/send from the controller and returns the report as an A2A artifact. Startup discovers tool schemas once over MCP. agent.py sends the conversation and tool schemas to the model, runs any requested tool calls over MCP, appends the results and asks again, until the model answers without tool calls.</em></p><h3>Deployment choices</h3><p>The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/setup-guide.md">setup guide</a> follows dependency order: cluster and storage, model credentials, monitoring and its token, kagent, MCP servers, the custom agent image, declarative agents, then tests. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/build-record.md">build record</a> preserves the order in which the original work actually happened.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_ysb44SGassNQExFEzL3NA.png" /></figure><p>The build stayed on the reviewed v0.9.10 resource model. The v1.0 alphas replace Agent with Harness/AgentTemplate and require a separate runtime; v0.10.3 was released after the original source review and was not evaluated. The exact versions and migration details are in the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/runtime-and-protocols.md#version-and-deployment-boundaries">reference</a>.</p><p>One instructive deployment issue was Grafana MCP tool discovery. Initializing through a local port-forward returned 200, while the controller’s in-cluster request returned Forbidden. Testing the same request with the Service-name Host header reproduced 403. The mcp/grafana:latest image allowed only loopback hosts by default. Adding the controller&#39;s Service hostname through --allowed-hosts, then triggering reconciliation, made all 65 tools discoverable. The committed values include the fix. <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/build-record.md#9-the-grafana-mcp-server-that-ships-with-kagent">Full investigation</a>.</p><h3>Method</h3><p>The faults are known in advance. Five workloads in demo-broken, each broken one way: a bad image tag (ImagePullBackOff), a command that prints a line and exits 1 (CrashLoopBackOff), 200 MiB allocated under a 64 MiB limit (OOMKilled), a Service selector that matches no pod (no endpoints), a 16-CPU request on 4-CPU nodes (Pending). A diagnosis counts as correct only if it names the configured fault and supports it with evidence. A completed task can still contain an incorrect answer.</p><p>The questions. Fourteen in <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/scripts/scenarios.txt">scripts/scenarios.txt</a>: six to the troubleshooter (one per fault, plus a “which pods are unhealthy” question), three to the observability analyst, one to the release manager, one to the runbook assistant, one to the bring-your-own agent (a full incident report), and two to the orchestrator. A script sent the initial pass through the controller with A2A message/send and saved the raw responses and answer text. A third orchestrator question, which asks for a fix, was added after the first two.</p><p>The approval flow was exercised twice against sre-operator with its own script: a scale request approved, and a scale request rejected, with the deployment&#39;s replica count checked before and after.</p><p>What was recorded. For each scenario: the task state, wall-clock seconds for the request, the number and names of tool calls (from the function-call parts in the A2A task history), and the answer. Tool counts for the bring-your-own agent come from its own progress messages, since it does not emit kagent’s metadata. Checks against the cluster used kubectl, helm list and a direct Prometheus query at the time of the run.</p><p>Scope. Specialist questions were run once. The orchestrator questions were retried through streaming and, for scenario 13, directly against its Service. No model was swapped on the same question, and timings were recorded in whole seconds. Per-scenario model cost was not captured.</p><p>Subsequent UI walkthrough (2026–10–13). The operator reported that the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/setup-guide.md#12-test-diagnosis-approval-and-orchestration">setup guide’s interactive examples</a> worked through the web UI. Supplied screenshots show diagnostic answers, node metrics, specialist delegation and approval/question prompts. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/build-record.md#follow-up-web-ui-validation-2026-10-13">follow-up record</a> documents this validation; the timings and verdicts below remain those measured on 2026–10–07.</p><h3>Results</h3><p>Measured on 2026–10–07 on the cluster described above. Specialist timings are wall-clock seconds for a single request. Orchestrator outcomes include the documented retries. Tool calls come from A2A task history or the custom reporter’s progress messages. Full answers and raw responses are in <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/results/answers">results/answers/</a>.</p><p><strong>Twelve questions to five of the six specialists, including the bring-your-own agent:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GJhDQ4rVnHssDLv4XAyecA.png" /></figure><p>Twelve of twelve completed; ten correct, one correct but incomplete, one partly wrong. All five faults were found by the troubleshooter one at a time, and all five again by the bring-your-own agent in one report.</p><p><strong>The approval flow, </strong><strong>sre-operator:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qd2AiwZOmi9id9TOfM8yyA.png" /></figure><p><strong>The orchestrator</strong> (sre-orchestrator, whose only tools are the six agents above):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JO1h-jWXV-ilnCC-qZE9hg.png" /></figure><p>Model spend for the day, including all scenarios and retries, was about 6 USD on OpenRouter. Per-request spend was not captured.</p><h3>Findings</h3><p>Declarative configuration was sufficient for the individual diagnoses. A nine-tool read-only agent diagnosed the five faults in 5–52 s with 4–8 calls per question. Its configuration is about 60 lines of YAML; no custom Kubernetes tool implementation was needed.</p><p>Delegation preserved the operator’s execution gate. The orchestrator’s tools were agent references, and the specialist’s approval pause surfaced through the parent. The successful repair and direct rejection test establish this behavior for the tested calls.</p><p>A completed task can still contain an incorrect answer. The troubleshooter classified batch as both healthy and unhealthy. The analyst missed four cluster-wide firing alerts. Scenario 14 combined metrics and resource evidence to distinguish node pressure from a container&#39;s own memory limit; it does not establish a general accuracy improvement from orchestration.</p><p>Streaming did not remove the controller timeout. The controller’s A2A client cut both plain responses and streams at 180 s. Go’s http.Client.Timeout covers reading the response body too. The recorded task remained working, without a final answer. Calling the agent Service directly bypasses this controller boundary, but the direct scenario 13 attempt was stopped on cost and never demonstrated completion.</p><p>Retries can overlap with abandoned work. Work continued in the pod after some client requests timed out. Three overlapping orchestrations exceeded the default 1 GiB memory limit. The manifest now gives the orchestrator 2 GiB, and subsequent tests were serialized. Logs showed the incident-reporter card fetched 77 times in a few minutes, suggesting retries inside delegation; that mechanism was not conclusively isolated.</p><p>Framework integration still needed operational investigation. Besides the Grafana Host check, the pinned Go image was unavailable, and the Prometheus tool rejected comparison operators as potentially dangerous characters. The latter was observed through live calls to the separate kagent-tools image, not established from its source. Raw error evidence and workarounds remain in the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/build-record.md">build record</a> and <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/runtime-and-protocols.md#tool-sources-and-discovery">protocol reference</a>.</p><p>Most of the day’s roughly 6 USD of model spend was attributed in the original notes to orchestration and abandoned runs. Per-scenario spend was not captured, so that attribution is not a measured breakdown.</p><h3>Limitations</h3><ul><li>One cluster and no repeated specialist trials. Fourteen initial scenarios and an additional repair scenario were tested on one cluster. Orchestrator questions were retried during troubleshooting. “Ten of twelve correct” is what happened, not an accuracy figure for these agents or models.</li><li>Known faults and pointed questions. The questions name the namespace and often the workload. A real incident starts with a symptom, not a resource name. The orchestrator scenarios are the closest this build comes to that.</li><li>Verification was partial for the metrics answers. The restart counts and firing alerts were checked against Prometheus at the time; the per-node CPU and memory shares were not recomputed by hand.</li><li>Not a production setup. The controller runs in its default unsecure auth mode (every caller is admin@kagent.dev), nothing is exposed outside the cluster, there is no NetworkPolicy, the tool server&#39;s Kubernetes permissions are the chart&#39;s defaults, and kmcp 0.3.0 adds no readiness or liveness probes to the pods it creates.</li><li>Pre-1.0 software. kagent 0.9.10 and kmcp 0.3.0. kagent’s 1.0 alphas replace the Agent resource model used here; 0.10.3 was released after the source review and was not evaluated.</li><li>The tool server is not part of the kagent source. kagent-tools 0.2.1 is a separate image. Its argument names and its PromQL filter (which rejects comparison operators) were taken from its live tools/list and from calling it, not from code.</li><li>Per-scenario cost was not recorded. The OpenRouter total for the day was about 6 USD; spend per scenario was not captured.</li><li>Not compared. No other agent runtime, no other way of hosting MCP servers, no controlled latency or throughput benchmark.</li></ul><h3>Reproduce it</h3><p>Start with the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/setup-guide.md">setup guide</a>. The <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/build-record.md">build record</a> contains every original command and output; the <a href="https://github.com/tharindu-sankalpa/eks-kagent-sre-agents/blob/main/docs/runtime-and-protocols.md">runtime and protocol reference</a> explains the resource fields, transports and API boundaries. All chart values and manifests are in infra/.</p><p>You need AWS access with eight spare on-demand vCPUs in the region, Docker, the command-line tools listed in the guide and an OpenRouter API key. AWS bills the EKS control plane, nodes, NAT gateway, storage and ECR images while they exist. The hourly total was not calculated; teardown is included.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1d427b96e2ea" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Benchmarking Qwen Models Across NVIDIA GPUs (T4, L4, H100) Architectures: Inference Performance &…]]></title>
            <link>https://medium.com/@wltsankalpa/benchmarking-qwen-models-across-nvidia-gpus-t4-l4-h100-architectures-finding-your-sweet-spot-a59a0adf9043?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/a59a0adf9043</guid>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Sun, 07 Sep 2025 14:29:09 GMT</pubDate>
            <atom:updated>2025-09-07T14:31:54.568Z</atom:updated>
            <content:encoded><![CDATA[<h3>Benchmarking Qwen Models Across NVIDIA GPUs (T4, L4, H100) Architectures: Inference Performance &amp; Code Quality Analysis</h3><p><em>Part 2 of 4: Deep Performance Analysis and Optimization</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xSHidhAj6MYp8rvHQtEbxw.png" /></figure><h3>Introduction: The Quest for Optimal Performance</h3><p>In Part 1, I successfully ran <strong>Qwen2.5-Coder-7B-Instruct</strong> on a NVIDIA T4 GPU using a notebook environment. I focused on loading the model into memory, running inference, and generating code samples. This demonstrated that self-hosted coding assistants are not only feasible but also functional today. However, if you’re serious about replacing commercial solutions like GitHub Copilot or Cursor, you need to understand the performance landscape across different GPU architectures and model variants.</p><p>This deep dive will answer critical questions:</p><ul><li>What are the underlying key technologies associated with different GPU architectures?</li><li>How do different NVIDIA GPUs (T4, L4, H100) impact model performance?</li><li>What are the real-world trade-offs between model sizes (7B vs 32B)?</li><li>Which optimizations actually matter for production inference?</li><li>How does code quality vary across different Qwen model variants?</li></ul><p>By the end of this article, I’ll share data-driven insights that can help you choose the optimal GPU and model combination for your specific needs, whether that’s maximizing throughput for a large team or optimizing cost for a startup.</p><h3>GPU Architecture Deep Dive: Understanding Your Hardware Options</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vns2D1M8dFMI8hAy.png" /><figcaption>Recap: Comprehensive GPU Comparison for LLM Workloads</figcaption></figure><p>Before examining specific hardware, it’s important to understand the technologies that impact LLM performance:</p><h4><strong>GPU Architecture: Silicon-Level Design Evolution</strong></h4><p>Think of GPU architecture as the blueprint that determines how efficiently your GPU can handle AI computations. Each generation brings significant improvements:</p><p><strong>Pascal (2016)</strong> was designed primarily for gaming and general computing. While it can run AI models, it lacks the specialized hardware that modern AI workloads demand. You’ll find yourself waiting significantly longer for model inference and training.</p><ul><li>Built on 16nm FinFET process node (GTX 1080)</li><li>Uses traditional CUDA cores with 32-bit floating-point units (FP32)</li><li>Memory bandwidth: 320 GB/s with GDDR5X</li><li><strong>Critical Limitation</strong>: No dedicated matrix multiplication units. AI workloads must use general-purpose ALUs (Arithmetic Logic Units), resulting in poor utilization. For matrix operations like A × B = C where A is 4096×4096, Pascal processes this as sequential multiply-accumulate operations across thousands of CUDA cores, creating memory bottlenecks and underutilization.</li></ul><p><strong>Turing (2018–2019)</strong> introduced the first-generation Tensor Cores, marking NVIDIA’s serious entry into AI acceleration. However, these early Tensor Cores are limited compared to what came later.</p><p><strong>Ampere (2020–2022) Architecture (RTX 3060, A100) </strong>represents a major leap forward with second and third-generation Tensor Cores. This architecture powers popular consumer cards like the RTX 3060 and 4070, as well as datacenter chips like the A100. The efficiency improvements here are substantial — you’re looking at 2–3x faster inference compared to Pascal.</p><ul><li>7nm Samsung process (consumer) / 7nm TSMC (datacenter)</li><li><strong>Third-generation Tensor Cores</strong>: Each Tensor Core can perform a 4×4 matrix multiplication in a single clock cycle</li><li><strong>Technical Detail</strong>: One Tensor Core operation computes D = A×B + C where A, B, C, D are 4×4 matrices using mixed precision (FP16 input, FP32 accumulation)</li><li>Memory subsystem improvements: 768 GB/s on A100 with HBM2</li><li><strong>Sparse Tensor Support</strong>: 2:4 structured sparsity allows 50% of weights to be zero while maintaining accuracy, effectively doubling throughput</li></ul><p><strong>Ada Lovelace (2022+)</strong> and <strong>Hopper (2022+)</strong> are the current state-of-the-art, with fourth-generation Tensor Cores and advanced memory systems that can handle the largest language models efficiently.</p><p><strong>Hopper Architecture (H100)</strong></p><ul><li>4nm TSMC process with 80 billion transistors</li><li><strong>Fourth-generation Tensor Cores</strong>: Support for FP8, BF16, FP16, FP32, and Transformer Engine</li><li><strong>DPX Instructions</strong>: Dedicated dynamic programming acceleration for algorithms like edit distance and Smith-Waterman</li><li><strong>Technical Breakthrough</strong>: Each SM (Streaming Multiprocessor) contains 4 fourth-gen Tensor Cores capable of 1,280 FP16 operations per clock cycle</li></ul><h4><strong>Compute Capability: Your CUDA Feature Set</strong></h4><p>This number determines which CUDA features your GPU can access. Here’s why it matters for AI:</p><ul><li><strong>6.x GPUs</strong> lack many modern optimizations and will struggle with current PyTorch and TensorFlow versions</li><li><strong>7.x-8.x GPUs</strong> support most modern AI frameworks and optimizations</li><li><strong>9.x GPUs</strong> unlock the latest features like advanced mixed-precision training and cutting-edge kernel optimizations</li></ul><p>In practical terms, a 6.1 GPU might take 10 seconds to generate a response that a 9.0 GPU completes in 2 seconds, purely due to architectural improvements.</p><h4>RAM: Your Model Size Ceiling</h4><p>VRAM directly determines which models you can run and how efficiently. Here’s the breakdown:</p><ul><li><strong>8GB</strong>: Limits you to 7B parameter models with basic quantization</li><li><strong>12GB</strong>: Comfortable for 7B models, can handle 13B models with optimization</li><li><strong>24GB</strong>: Opens up 13B-30B models, essential for serious development work</li><li><strong>80GB+</strong>: Required for the largest models (70B+ parameters) without aggressive quantization</li></ul><p>The relationship isn’t linear; a model that barely fits in VRAM will run slowly due to memory pressure. You should aim for at least 20% headroom for optimal performance.</p><h4>Tensor Cores: The AI Acceleration Engine</h4><p>Tensor Cores are specialized matrix multiplication units designed specifically for AI workloads. Here’s what each generation brings:</p><p><strong>No Tensor Cores</strong>: Your GPU falls back to standard CUDA cores, which are roughly 10x slower for AI matrix operations. A model inference that takes 2 seconds on a modern GPU might take 20+ seconds.</p><p><strong>2nd Generation</strong>: Provides significant acceleration for FP16 operations. You’ll see 3–5x speedup compared to CUDA cores for transformer models.</p><p><strong>4th Generation</strong>: The current flagship technology, supporting multiple precision formats (FP16, BF16, INT8, FP8) and delivering up to 10x speedup over CUDA cores. These also include sparsity support, which can double performance for compatible models.</p><h4>FlashAttention 2: Memory-Efficient Attention</h4><p>FlashAttention 2 addresses the memory bottleneck in transformer models. Here’s the technical reality:</p><p><strong>Without FlashAttention 2</strong>: The attention mechanism in transformers has quadratic memory complexity. For a sequence length of 4,096 tokens, standard attention requires storing a 4,096 × 4,096 matrix, consuming massive amounts of memory and bandwidth.</p><p><strong>With FlashAttention 2</strong>: The algorithm restructures attention computation to use memory hierarchically — keeping frequently accessed data in faster memory tiers. This reduces memory usage by 2–4x and increases speed by 1.5–2x for long sequences.</p><p><strong>Practical Impact</strong>: A model that can only handle 2,048-token contexts without FlashAttention 2 might handle 8,192-token contexts with it. For code generation, this means understanding much larger codebases and maintaining context across longer conversations.</p><h4>FP8 Support: Next-Generation Precision</h4><p>FP8 (8-bit floating point) represents the cutting edge of model optimization:</p><p><strong>Traditional FP16</strong>: Uses 16 bits per parameter, providing good precision but consuming significant memory and bandwidth.</p><p><strong>FP8</strong>: Reduces each parameter to 8 bits while maintaining model quality through advanced scaling techniques. This effectively doubles your model capacity — you can run a 14B parameter model in the same memory footprint that previously held a 7B model.</p><p><strong>Real Performance Impact</strong>: FP8 can deliver 1.5–2x inference speedup while reducing memory usage by 40–50%. However, it requires careful calibration and isn’t universally supported across all model architectures yet.</p><p>Now, let’s understand what makes each GPU unique for LLM inference.</p><h4>NVIDIA Tesla T4: The Budget-Conscious Choice</h4><p>The T4, with its 16GB of VRAM and compute capability 7.5, represents the entry point for production LLM deployment. Built on the Turing architecture, it was NVIDIA’s first GPU to include tensor cores specifically designed for AI workloads.</p><p><strong>Key Specifications:</strong></p><ul><li><strong>Architecture</strong>: Turing (12nm process)</li><li><strong>VRAM</strong>: 16 GB GDDR6</li><li><strong>Memory Bandwidth</strong>: 320 GB/s</li><li><strong>Tensor Cores</strong>: 320 (1st generation)</li><li><strong>FP16 Performance</strong>: 65 TFLOPS</li><li><strong>TDP</strong>: 70W</li><li><strong>Typical Cloud Cost</strong>: $0.35–0.50/hour</li></ul><p><strong>For LLM Inference:</strong></p><ul><li>✅ Can run 7B models comfortably</li><li>⚠️ Tight squeeze for 14B models</li><li>❌ Cannot run 30B+ models without quantization</li><li>Limited to older optimization techniques</li></ul><h4>NVIDIA L4: The Production Sweet Spot</h4><p>The L4 represents NVIDIA’s latest efficient inference GPU, built on the Ada Lovelace architecture with significant improvements over the T4.</p><p><strong>Key Specifications:</strong></p><ul><li><strong>Architecture</strong>: Ada Lovelace (4nm process)</li><li><strong>VRAM</strong>: 24 GB GDDR6</li><li><strong>Memory Bandwidth</strong>: 300 GB/s</li><li><strong>Tensor Cores</strong>: 240 (4th generation)</li><li><strong>FP16 Performance</strong>: 121 TFLOPS</li><li><strong>FP8 Performance</strong>: 242 TFLOPS</li><li><strong>TDP</strong>: 72W</li><li><strong>Typical Cloud Cost</strong>: $0.50–0.75/hour</li></ul><p><strong>For LLM Inference:</strong></p><ul><li>✅ Excellent for 7B models with headroom</li><li>✅ Can handle 14B models effectively</li><li>⚠️ Possible to run quantized 30B models</li><li>Supports modern optimizations including FlashAttention 2</li></ul><h4>NVIDIA H100 NVL: The Performance Monster</h4><p>The H100 NVL with 94GB of HBM3 memory represents the pinnacle of inference performance. Built on the Hopper architecture, it includes revolutionary features like the Transformer Engine designed specifically for LLMs.</p><p><strong>Key Specifications:</strong></p><ul><li><strong>Architecture</strong>: Hopper (4nm process)</li><li><strong>VRAM</strong>: 94 GB HBM3</li><li><strong>Memory Bandwidth</strong>: 3.9 TB/s</li><li><strong>Tensor Cores</strong>: 528 (4th generation with Transformer Engine)</li><li><strong>FP16 Performance</strong>: 1,979 TFLOPS</li><li><strong>FP8 Performance</strong>: 3,958 TFLOPS</li><li><strong>TDP</strong>: 400W (NVL variant)</li><li><strong>Typical Cloud Cost</strong>: $4.00–5.00/hour</li></ul><p><strong>For LLM Inference:</strong></p><ul><li>✅ Runs any current open-source model</li><li>✅ Multiple 32B models simultaneously</li><li>✅ Native FP8 support for maximum efficiency</li><li>Full support for all cutting-edge optimizations</li></ul><h3>Understanding Modern GPU Optimizations</h3><p>Before presenting benchmark results, let’s demystify the key optimizations that dramatically impact performance on modern GPUs.</p><h4>FlashAttention 2: Solving the Attention Bottleneck</h4><p>Traditional attention mechanisms are the primary bottleneck in transformer inference. Here’s why FlashAttention 2 is revolutionary:</p><p><strong>The Problem:</strong></p><pre># Traditional attention (simplified)<br>def standard_attention(Q, K, V):<br>    # Compute attention scores: O(n²) memory<br>    scores = torch.matmul(Q, K.transpose(-2, -1))<br>    attention_weights = torch.softmax(scores / sqrt(d_k), dim=-1)<br>    output = torch.matmul(attention_weights, V)<br>    return output</pre><p>For a 32K sequence, this requires storing a 32K × 32K attention matrix — that’s 4GB just for attention scores!</p><p><strong>The FlashAttention 2 Solution:</strong></p><pre># FlashAttention 2 concept (pseudocode)<br>def flash_attention_v2(Q, K, V):<br>    # Process in blocks that fit in GPU SRAM<br>    for block in blocks:<br>        # Compute local attention in fast memory<br>        local_output = compute_block_attention(Q[block], K, V)<br>        accumulate(local_output)<br>    return final_output</pre><p><strong>Real-world Impact:</strong></p><ul><li><strong>Memory</strong>: O(n) instead of O(n²)</li><li><strong>Speed</strong>: 2–4x faster on long sequences</li><li><strong>Context</strong>: Enables 128K+ token contexts</li></ul><h4>BFloat16 (BF16) vs Float16: Precision Matters</h4><p>Understanding numerical formats is crucial for optimization:</p><p><strong>Float32 (FP32)</strong>: Standard precision</p><ul><li>Sign: 1 bit, Exponent: 8 bits, Mantissa: 23 bits</li><li>Range: ±3.4 × 1⁰³⁸</li><li>Used for: Training, high-precision inference</li></ul><p><strong>Float16 (FP16)</strong>: Half precision</p><ul><li>Sign: 1 bit, Exponent: 5 bits, Mantissa: 10 bits</li><li>Range: ±65,504</li><li>Problem: Can overflow/underflow with large models</li></ul><p><strong>BFloat16 (BF16)</strong>: Brain Float</p><ul><li>Sign: 1 bit, Exponent: 8 bits, Mantissa: 7 bits</li><li>Range: ±3.4 × 1⁰³⁸ (same as FP32!)</li><li>Advantage: Better numerical stability for LLMs</li></ul><pre># BF16 usage on H100<br>model = AutoModelForCausalLM.from_pretrained(<br>    model_name,<br>    torch_dtype=torch.bfloat16,  # Better stability than float16<br>    device_map=&quot;cuda:0&quot;<br>)</pre><h4>PyTorch 2.0 Compilation: Graph-Level Optimization</h4><p>torch.compile() transforms your model into optimized CUDA kernels:</p><p><strong>Without Compilation:</strong></p><ul><li>Each operation is a separate CUDA kernel launch</li><li>Memory transfers between operations</li><li>No cross-operation optimization</li></ul><p><strong>With Compilation:</strong></p><pre># Enable compilation<br>model = torch.compile(model, mode=&quot;reduce-overhead&quot;)<br><br># What happens internally:<br># 1. Traces model execution graph<br># 2. Fuses compatible operations<br># 3. Generates optimized CUDA kernels<br># 4. Reduces memory transfers</pre><p><strong>Optimization Modes:</strong></p><ul><li>&quot;default&quot;: Balanced optimization</li><li>&quot;reduce-overhead&quot;: Minimize kernel launches (best for inference)</li><li>&quot;max-autotune&quot;: Maximum optimization (longer compile time)</li></ul><h3>Comprehensive Benchmarking Framework</h3><p>To ensure fair comparisons across GPUs and models, I developed a unified benchmarking framework that tests real-world code generation capabilities.</p><p><strong>📊 Full Benchmarking Notebook</strong>: <a href="https://github.com/tharindu-sankalpa/huggingface-learning/blob/main/qwen_exploration.ipynb">View on GitHub</a></p><h4>The Benchmark Task</h4><p>I chose a production-relevant prompt that tests multiple aspects of code generation:</p><pre>I need to create a FastAPI server to serve the HuggingFace Vision Transformer model, <br>Falconsai/nsfw_image_detection. The API should follow best practices and be optimized <br>for high inference performance. Please provide the complete implementation.</pre><p>This task evaluates:</p><ul><li><strong>Framework knowledge</strong>: FastAPI implementation</li><li><strong>ML integration</strong>: HuggingFace transformers</li><li><strong>Best practices</strong>: Error handling, optimization</li><li><strong>Completeness</strong>: Full working solution</li></ul><h4>Models Under Test</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JBA_w6rdp6MfPMllGOExdA.png" /></figure><h4>Benchmarking Methodology</h4><p>Our framework measures:</p><ol><li><strong>Load Time</strong>: Model initialization to ready state</li><li><strong>Memory Usage</strong>: VRAM allocation and overhead</li><li><strong>Time to First Token</strong>: Response latency</li><li><strong>Tokens per Second</strong>: Sustained generation speed</li><li><strong>Total Generation Time</strong>: End-to-end performance</li><li><strong>Code Quality</strong>: Manual evaluation of outputs</li></ol><h3>Performance Results: T4 GPU Benchmarks</h3><p>The T4 represents the minimum viable hardware for production deployment. Here’s how each model performs:</p><h4>Memory Constraints on T4 (16GB VRAM)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3x4_ehVzIyvbjZcEXKTYGg.png" /></figure><p><strong>Key Finding</strong>: Only the 7B model fits on T4, and even then with minimal headroom for KV cache during generation.</p><h4>Qwen2.5-Coder-7B-Instruct Performance on T4</h4><pre>🕒 Generation time: 81.38s<br>💾 Memory delta during generation: 0.009 GB<br>📝 Generated tokens: 307<br>⚡ Tokens/second: 3.8</pre><p><strong>Analysis</strong>:</p><ul><li>Generation is functional but slow (3.8 tok/s)</li><li>Limited memory prevents long-context generation</li><li>Suitable only for batch processing or non-real-time use</li></ul><h4>Optimization Attempts on T4</h4><p>Even with optimizations, T4 performance remains limited:</p><pre># T4 optimizations attempted<br>model = model.half()  # FP16 precision<br>model.gradient_checkpointing_enable()  # Save memory<br>torch.backends.cudnn.benchmark = True  # Optimize convolutions<br><br># Results: Marginal improvement to ~4.2 tok/s</pre><h3>Performance Results: L4 GPU Benchmarks</h3><p>The L4 offers a significant upgrade with 24GB VRAM and modern architecture.</p><h4>Memory Capabilities on L4</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*k-WQtqtbv5-wGXaoDprLBA.png" /></figure><h4>Qwen2.5-Coder-7B-Instruct Performance on L4</h4><p>With L4’s improved architecture and FlashAttention 2 support:</p><pre>🕒 Generation time: 18.2s<br>💾 Memory delta during generation: 0.4 GB<br>📝 Generated tokens: 550<br>⚡ Tokens/second: 30.2</pre><p><strong>Improvements over T4:</strong></p><ul><li><strong>8x faster inference</strong> (30.2 vs 3.8 tok/s)</li><li><strong>Better memory headroom</strong> for concurrent requests</li><li><strong>FlashAttention 2 enabled</strong> for efficiency</li><li><strong>Production-viable</strong> for real-time applications</li></ul><h4>L4 Optimization Stack</h4><pre># L4-specific optimizations<br>model = AutoModelForCausalLM.from_pretrained(<br>    model_name,<br>    torch_dtype=torch.float16,<br>    attn_implementation=&quot;flash_attention_2&quot;,  # Key enabler<br>    device_map=&quot;cuda:0&quot;<br>)<br><br># Additional optimizations<br>torch.backends.cuda.matmul.allow_tf32 = True<br>model = torch.compile(model, mode=&quot;reduce-overhead&quot;)<br># Results: Boosted to ~35-40 tok/s</pre><h3>Performance Results: H100 NVL Benchmarks</h3><p>The H100 NVL unlocks the full potential of all models with its massive memory and compute capabilities.</p><h4>Complete Model Compatibility</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ECc7rtu2L-oo3AaNtNLk6w.png" /></figure><h4>Comprehensive Performance Metrics on H100</h4><p>Our benchmarking framework tested all five models with identical prompts and optimization settings on the H100 NVL:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KUqiVTOXx0WozzjLfALiyw.png" /></figure><h4>H100 Optimization Implementation</h4><p>The H100’s Hopper architecture enables advanced optimizations that dramatically improve inference performance:</p><pre># H100-optimized configuration<br>os.environ[&quot;PYTORCH_CUDA_ALLOC_CONF&quot;] = &quot;max_split_size_mb:512,expandable_segments:True&quot;<br><br>model = AutoModelForCausalLM.from_pretrained(<br>    model_name,<br>    torch_dtype=torch.bfloat16,  # BF16 optimal for H100<br>    device_map=&quot;cuda:0&quot;,<br>    attn_implementation=&quot;flash_attention_2&quot;,<br>    max_memory={&quot;cuda:0&quot;: &quot;85GB&quot;}  # Leave headroom for KV cache<br>).eval()<br><br># Enable H100-specific features<br>torch.backends.cuda.matmul.allow_tf32 = True<br>torch.backends.cudnn.allow_tf32 = True<br>torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction = True<br><br># Compile for maximum performance<br>model = torch.compile(model, mode=&quot;reduce-overhead&quot;)</pre><h3>Comparative Analysis: Tokens per Second Across GPUs</h3><p>Here’s how the same model (Qwen2.5-Coder-7B-Instruct) performs across different GPUs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x-INWfp1ngQWlLEKg7AMRQ.png" /></figure><p><strong>Key Insights:</strong></p><ul><li>L4 offers the best cost efficiency for 7B models</li><li>H100’s advantage becomes clear with larger models</li><li>T4 is viable only for non-real-time applications</li></ul><h3>Code Quality Assessment: Beyond Raw Performance</h3><p>Beyond raw numbers and tokens per second lies the critical question that every production deployment faces: <strong>Does the generated code actually work?</strong> After completing our performance benchmarks, I needed to answer this fundamental question by actually running and testing each implementation.</p><h4>The Benchmark Task: A Production-Relevant Challenge</h4><p>To ensure fair evaluation across all models, I used a standardized prompt that tests multiple aspects of professional software development:</p><blockquote>“I need to create a FastAPI server to serve the HuggingFace Vision Transformer model, Falconsai/nsfw_image_detection. The API should follow best practices and be optimized for high inference performance. Please provide the complete implementation.”</blockquote><p>This task evaluates several critical competencies:</p><ul><li><strong>Framework Knowledge</strong>: Understanding of FastAPI patterns and conventions</li><li><strong>ML Integration</strong>: Proper handling of HuggingFace transformers and model loading</li><li><strong>Best Practices</strong>: Error handling, security considerations, and API design</li><li><strong>Performance Optimization</strong>: GPU utilization, memory management, and inference efficiency</li><li><strong>Completeness</strong>: Ability to generate a full, working solution rather than code snippets</li></ul><p>The reality isn’t as straightforward as the performance metrics suggest. While our benchmarking framework successfully measured speed and memory usage across all five models, the evaluation of generated code was a manual and complex task.</p><h4>The Reality Check: Testing Generated Code in Production</h4><p>I took each model’s FastAPI implementation and attempted to deploy it locally, documenting every issue encountered along the way. The complete generated outputs are available in my <a href="https://github.com/tharindu-sankalpa/huggingface-learning/tree/main/qwen_codegen_outputs-flash">GitHub repository</a> for full transparency.</p><h4>Qwen2.5-Coder-7B-Instruct: Functional but Requires Fixes</h4><p><strong>📊 Performance</strong>: 53.1 tok/s | <strong>📁 Code</strong>: <a href="https://github.com/tharindu-sankalpa/huggingface-learning/blob/main/qwen_codegen_outputs-flash/Qwen2.5-Coder-7B-Instruct__20250803_073223.md">View Generated Output</a></p><p>The Qwen2.5-Coder-7B-Instruct’s FastAPI implementation didn’t work out of the box due to a missing Torch import. However, once that was added, it worked. That said, we still had to provide the image as a URL appended directly to the endpoint, and there didn’t seem to be any proper HTTP URL validation in place. Still, it functioned after those adjustments.</p><p>What Actually Worked:</p><pre># After adding missing import torch<br>@app.post(&quot;/predict/&quot;)<br>async def predict(image_url: str):<br>    try:<br>        response = requests.get(image_url)  # No timeout or validation<br>        image = Image.open(BytesIO(response.content))<br>        # ... rest of the inference pipeline</pre><p>For a production-grade implementation, though, there are numerous areas that need improvement. There are no file size limits, no timeouts for image downloads, and no protection against malicious URLs. Error handling is also lacking — for instance, there’s no response if the URL doesn’t point to a valid image, if the image is corrupted, or if it’s in an unsupported format.</p><p>Additionally, there’s no proper health check, no logging, and no API documentation. So overall, while it technically works after some fixes, it falls short of production-readiness in several critical aspects.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QJQAo37Ok75DSqx9UkYhxA.png" /><figcaption>Testing the 7B model’s API with Postman — basic functionality works but lacks robustness</figcaption></figure><h4>Qwen2.5-Coder-32B-Instruct: Professional Implementation</h4><p><strong>📊 Performance</strong>: 25.5 tok/s | <strong>📁 Code</strong>: <a href="https://github.com/tharindu-sankalpa/huggingface-learning/blob/main/qwen_codegen_outputs-flash/Qwen2.5-Coder-32B-Instruct__20250803_074142.md">View Generated Output</a></p><p>The Qwen2.5-Coder-32B-Instruct’s implementation worked out of the box and felt closer to a production-grade setup. Instead of relying on image URLs, it accepts image uploads directly, which is a safer and more standard approach for APIs like this.</p><p><strong>Professional Features:</strong></p><pre>class Prediction(BaseModel):<br>    label: str<br>    score: float<br><br>def load_image(image_file: UploadFile) -&gt; Image.Image:<br>    try:<br>        contents = image_file.file.read()<br>        image = Image.open(io.BytesIO(contents))<br>        return image.convert(&#39;RGB&#39;)<br>    except Exception as e:<br>        raise HTTPException(status_code=400, detail=f&quot;Error loading image: {e}&quot;)</pre><p>The code uses proper model loading with AutoImageProcessor and handles device assignment for CUDA, which is a good touch. It also defines a clear Prediction response model and includes a health check endpoint, which was missing in the 7B version. However, there’s still room for improvement; there’s no limit on the file size for uploads, no handling for potentially corrupted or unsupported image formats beyond a generic try-catch, and no explicit logging or timeout configurations. API documentation could be enhanced further by defining error response models or describing file format expectations more clearly. But overall, this version is significantly more robust and better aligned with good API design practices compared to the first one.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sK4KMW3IyuUaLpttLwkDeA.png" /><figcaption>Professional file upload approach in action — demonstrating proper error handling and structured response format with the 32B model</figcaption></figure><h4>Qwen3–32B: Verbose but Incomplete</h4><p><strong>📊 Performance</strong>: 21.7 tok/s | <strong>📁 Code</strong>: <a href="https://github.com/tharindu-sankalpa/huggingface-learning/blob/main/qwen_codegen_outputs-flash/Qwen3-32B__20250803_075452.md">View Generated Output</a></p><p>The general-purpose Qwen3–32B generated 4096 tokens but failed to provide a complete, focused implementation. The Qwen3–32B’s implementation also ran into an issue due to the @limiter.limit() decorator being applied to a function that doesn&#39;t explicitly accept a request parameter; slowapi needs this to identify the client IP. After fixing this, it worked and was the most complete so far, with multiple production-grade enhancements like API key authentication, rate limiting, and support for batch image processing with multiple images.</p><p>Advanced Features (After Fixes):</p><pre># Rate limiting and authentication<br>@app.post(&quot;/predict/&quot;)<br>@limiter.limit(&quot;10/minute&quot;)<br>async def predict(<br>    request: Request,  # Added missing parameter<br>    files: List[UploadFile] = File(...),<br>    api_key: str = Depends(get_api_key)<br>):</pre><p>It uses FastAPI effectively and introduces slowapi for rate limiting to guard against abuse, while also requiring an API key via the Authorization header for secured access. It properly offloads computation to CUDA if available, and includes graceful error handling for invalid images.</p><p>Apart from that, file size limits and explicit logging are still missing, and the rate limit response could include retry headers for better client-side handling. Overall, this version is the closest to a solid production API, pending some fixes and refinements.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wsITRZQZzU2xcfMbX0oqyA.png" /><figcaption>Advanced features working after fixes — showcasing API key authentication and rate limiting in practice with batch processing capabilities</figcaption></figure><p>The model spent excessive tokens on explanation rather than implementation, ultimately producing incomplete code that cuts off mid-function. This highlights the importance of task-specific training for focused coding tasks.</p><h4>Qwen3–30B-A3B: Clean and Minimal</h4><p><strong>📊 Performance</strong>: 10.6 tok/s | <strong>📁 Code</strong>: <a href="https://github.com/tharindu-sankalpa/huggingface-learning/blob/main/qwen_codegen_outputs-flash/Qwen3-30B-A3B__20250803_081100.md">View Generated Output</a></p><p>This implementation is clean and minimal, worked out of the box without any debugging fixes or refactoring, focusing on functionality while incorporating some production-aware practices. It correctly checks for GPU availability, uses structured logging for model loading and prediction errors, and validates that uploaded files are indeed images. The use of model.eval() ensures the model is in inference mode, and the softmax probabilities along with class labels are returned clearly.</p><p><strong>Solid Foundation:</strong></p><pre># Check file type<br>if not file.content_type.startswith(&#39;image/&#39;):<br>    raise HTTPException(status_code=400, detail=&quot;File must be an image&quot;)<br><br># Structured error handling<br>except Exception as e:<br>    logging.error(f&quot;Error during prediction: {e}&quot;)<br>    raise HTTPException(status_code=500, detail=str(e))</pre><p>While this version doesn’t include advanced features like rate limiting, authentication, or batch processing, it does include basic error handling and type checking, which is a solid foundation. Still, there’s room for improvement — like enforcing file size limits, expanding error responses for edge cases (e.g., unsupported image formats), and separating environment-specific variables (like model name) into a config. Overall, it’s a straightforward and effective base version that would work well in controlled environments or for internal use.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kNhLBlJ9_9UTx1oDTfZgUg.png" /><figcaption>Clean implementation in action — demonstrating the minimal but effective approach with proper validation and logging</figcaption></figure><h4>Qwen3-Coder-30B-A3B-Instruct: Enterprise-Grade Excellence</h4><p><strong>📊 Performance</strong>: 10.3 tok/s | <strong>📁 Code</strong>: <a href="https://github.com/tharindu-sankalpa/huggingface-learning/blob/main/qwen_codegen_outputs-flash/Qwen3-Coder-30B-A3B-Instruct__20250803_083822.md">View Generated Output</a></p><p>This version is the most feature-complete and production-ready of all the implementations. It introduces structured initialization using lifespan, supports both single and batch image predictions, and adds important engineering practices like concurrent processing with a thread pool, a health check endpoint, CORS middleware, and detailed structured logging.</p><p><strong>Production Deployment Test:</strong></p><pre>(huggingface-learning) tharindu@192 qwen_codegen_outputs-flash % python main.py<br>/Users/tharindu/git/huggingface-learning/qwen_codegen_outputs-flash/main.py:226: DeprecationWarning: <br>        on_event is deprecated, use lifespan event handlers instead.<br><br>        Read more about it in the<br>        [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/).<br>        <br>  @app.on_event(&quot;startup&quot;)<br>INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)<br>INFO:     Started parent process [28697]<br>INFO:     Started server process [28712]<br>INFO:     Waiting for application startup.<br>INFO:main:Loading NSFW detection model...<br>INFO:     Started server process [28715]<br>INFO:     Waiting for application startup.<br>INFO:main:Loading NSFW detection model...<br>INFO:     Started server process [28713]<br>INFO:     Waiting for application startup.<br>INFO:     Started server process [28714]<br>INFO:     Waiting for application startup.<br>INFO:main:Loading NSFW detection model...<br>INFO:main:Loading NSFW detection model...<br>INFO:main:Using CPU for inference<br>INFO:main:Using CPU for inference<br>INFO:main:Model loaded successfully<br>INFO:     Application startup complete.<br>INFO:main:Model loaded successfully<br>INFO:     Application startup complete.<br>INFO:main:Using CPU for inference<br>INFO:main:Model loaded successfully<br>INFO:     Application startup complete.<br>INFO:main:Using CPU for inference<br>INFO:main:Model loaded successfully<br>INFO:     Application startup complete.<br>INFO:     127.0.0.1:57719 - &quot;POST /predict HTTP/1.1&quot; 200 OK</pre><p>Enterprise Features:</p><pre>@asynccontextmanager<br>async def lifespan(app: FastAPI):<br>    &quot;&quot;&quot;Initialize model at startup and cleanup on shutdown&quot;&quot;&quot;<br>    global model, processor<br>    try:<br>        # Proper lifecycle management<br>        model = ViTForImageClassification.from_pretrained(&quot;Falconsai/nsfw_image_detection&quot;)<br>        # GPU optimization<br>        if torch.cuda.is_available():<br>            model = model.cuda()<br>        model.eval()<br>        yield<br>    finally:<br>        executor.shutdown(wait=True)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jTE7S0Ei4qnpNEx-DM0kHQ.png" /><figcaption>Enterprise-grade API in production — showcasing the sophisticated lifespan management, health checks, and concurrent processing capabilities</figcaption></figure><p>The use of Pydantic models for both input and output improves the API’s clarity and documentation. The app cleanly handles CPU/GPU logic, properly sets the model to evaluation mode, and includes graceful error handling across the board. There’s also a performance optimization hook via the on_event(&quot;startup&quot;) for enabling CUDNN benchmarks, though it triggers a deprecation warning since FastAPI now recommends lifespan handlers. Minor improvements could include better request validation in the batch endpoint (e.g., checking image formats or size before inference) and rate limiting or API key authentication for additional protection. Still, overall this version is a robust, scalable, and extensible foundation for an NSFW detection service.</p><h3>Conclusion</h3><p>After extensive benchmarking across three distinct GPU architectures and comprehensive code quality analysis, several clear patterns emerge that can guide your infrastructure decisions.</p><h4>The Performance Hierarchy</h4><p>The hardware performance results align with expectations but reveal important nuances. The H100 NVL delivers exceptional capabilities, enabling deployment of any current open-source model with room for concurrent instances. However, its $4–5/hour cloud cost makes it economically viable only for high-throughput production environments or applications where inference quality significantly impacts revenue.</p><p>The L4 emerges as the practical sweet spot for most deployments. At $0.50–0.75/hour, it provides 8x better performance than the T4 while supporting modern optimizations like FlashAttention 2. For the Qwen2.5-Coder-7B model, this translates to production-viable response times that can support real-time coding assistance applications.</p><p>The T4, while budget-friendly at $0.35–0.50/hour, presents significant limitations. With inference speeds of 3.8 tokens/second, it restricts you to batch processing scenarios or applications where users can tolerate multi-second response delays.</p><h4>Code Quality Insights That Matter</h4><p>The code quality analysis reveals more significant differences than the performance benchmarks alone suggest. Specialized code models consistently outperform general-purpose variants, but the gap is larger than expected. The Qwen2.5-Coder series generates substantially more production-ready implementations compared to the general Qwen3–32B model, despite similar parameter counts.</p><p>Model size correlates with code sophistication, but with diminishing returns beyond 32B parameters for most practical applications. The 32B specialized model produces professional-grade implementations that require minimal modification for production deployment, while the 7B model generates functional prototypes that need additional engineering work.</p><p>Critically, all generated code requires some manual intervention. The “out of the box” functionality varies dramatically, from simple import fixes to substantial security and architecture improvements. This reality should inform your development workflow expectations and resource allocation.</p><h4>Practical Deployment Recommendations</h4><p>For startups and small teams prioritizing cost efficiency, the combination of Qwen2.5-Coder-7B-Instruct on L4 GPUs provides the optimal balance. The 53.1 tokens/second generation speed supports real-time applications, while the 15GB memory footprint allows multiple concurrent users on a single GPU instance.</p><p>Organizations requiring higher code quality and willing to trade speed for sophistication should consider Qwen2.5-Coder-32B-Instruct on H100 hardware. The 25.5 tokens/second performance remains production-viable while delivering significantly more robust implementations.</p><p>Enterprise environments with complex requirements and budget flexibility can leverage the Qwen3-Coder-30B-A3B-Instruct model on H100 infrastructure. Despite slower generation (10.3 tokens/second), it produces enterprise-grade code with advanced architectural patterns that reduce downstream development time.</p><h4>The Infrastructure Reality</h4><p>Moving beyond benchmarks to actual deployment introduces additional considerations. Memory utilization patterns during inference require 20–30% headroom beyond model size for optimal performance. Network latency between GPU instances and application servers can impact user experience more than raw inference speed in distributed architectures.</p><p>The choice between cloud and on-premises deployment depends significantly on usage patterns. Consistent daily usage of 8+ hours makes dedicated hardware economical, while sporadic or development usage favors cloud instances with their flexibility and reduced operational overhead.</p><h3>What Part 3 Will Cover</h3><p>These performance and quality insights directly inform our next phase: building and deploying a production-ready code generation API on Google Kubernetes Engine (GKE). Part 3 will focus on implementing a complete FastAPI-based coding assistant that loads the Qwen2.5-Coder-7B-Instruct model and serves code generation requests efficiently on L4 GPU infrastructure.</p><p>The core implementation will center around creating a robust FastAPI application that handles the entire code generation pipeline, from request validation through model inference to response formatting. We’ll implement proper model loading strategies, optimize memory usage patterns, and establish request/response handling that can scale under production load on GKE’s managed infrastructure.</p><p>A critical focus will be <strong>model artifact management; </strong>handling the substantial .safetensors file collections that comprise the Qwen2.5-Coder-7B model from HuggingFace repositories. We’ll explore storage strategies including Persistent Volume Claims (PVCs) with Google Cloud Storage integration, and local SSD configurations to optimize model loading times. The challenge lies in balancing storage costs, I/O performance, and pod startup times when dealing with multi-gigabyte model files across a distributed cluster.</p><p>We’ll create optimized Docker images that properly handle GPU dependencies, model artifacts, and runtime configurations while addressing common deployment challenges like model loading times and memory management.</p><p><strong>L4 GPU configuration on GKE</strong> will receive major attention, covering node pool setup, GPU sharing strategies, and the specific NVIDIA device plugin configurations required for our containerized model access. We’ll tackle node labeling, resource allocation, scheduling constraints, and the GKE-specific optimizations that ensure efficient GPU utilization for transformer inference workloads.</p><p>Storage architecture decisions, whether to use ephemeral storage with fast model downloads, persistent volumes for model caching, or hybrid approaches, will directly impact both performance and cost efficiency. We’ll examine the trade-offs between Google Cloud Storage, Persistent Disks, and local SSD storage for different usage patterns and scaling requirements.</p><p>The goal is translating these benchmark insights into a production-ready coding assistant platform deployed on GKE that can reliably serve real users with competitive response times while managing infrastructure costs effectively.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a59a0adf9043" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Break Free from GitHub Copilot: Deploy Your Own Qwen2.5-Coder Assistant]]></title>
            <link>https://medium.com/@wltsankalpa/break-free-from-github-copilot-deploy-your-own-qwen2-5-coder-assistant-9c94153cc235?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/9c94153cc235</guid>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Sun, 03 Aug 2025 13:06:13 GMT</pubDate>
            <atom:updated>2025-08-03T13:06:13.209Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Part 1 of 4: Foundation and Initial Setup</em></p><blockquote>The AI coding assistant revolution has fundamentally transformed how developers write code. Tools like GitHub Copilot, OpenAI Codex, Claude, and Cursor have become indispensable for millions of developers worldwide. But as these tools mature and teams become increasingly dependent on them, a critical question emerges: <strong>Should you continue paying premium prices while surrendering control over your most sensitive asset — your code?</strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lgXagDz9spscLzC_gpafQw.png" /></figure><p>For many development teams, the answer is increasingly “no.” The limitations of commercial solutions — escalating costs, data privacy concerns, vendor lock-in, and lack of customization — have sparked a growing movement toward self-hosted alternatives. This four-part series will guide you through deploying your own production-ready coding assistant using Qwen2.5-Coder models, giving you the power, privacy, and cost control that commercial solutions can’t match.</p><p>In this first installment, we’ll establish why self-hosting matters, explore the Qwen model ecosystem, and get you up and running with a working coding assistant in under an hour.</p><h3>💰The Real Economics of Self-Hosted AI Coding Assistants</h3><h4>Beyond Simple Cost Comparison: Understanding True Infrastructure Needs</h4><p>Let’s be honest about what it takes to match commercial coding assistant performance. Tools like GitHub Copilot and Cursor aren’t running 7B parameter models — they’re powered by models comparable to GPT-4, which means we need to think seriously about infrastructure requirements.</p><p><strong>To compete with commercial solutions, you need</strong>:</p><ul><li><strong>Qwen2.5-Coder-32B-Instruct</strong> (minimum for competitive performance)</li><li><strong>Multiple H100 GPUs</strong> for reasonable latency at scale</li><li><strong>Proper load balancing</strong> and inference serving infrastructure</li></ul><h4>Realistic Cost Analysis for Competitive Performance</h4><p>For a development team of 100 engineers expecting Copilot-level performance:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TznquZBP8r_v88Pp5J3Xcw.png" /><figcaption><em>Infrastructure costs include GPU compute ($3,500–4,000/month per H100), networking, storage, monitoring, and operational overhead.</em></figcaption></figure><h4>Why the Higher Cost Is Worth It</h4><p><strong>The numbers don’t lie</strong>: For pure cost optimization, commercial solutions win. But here’s why forward-thinking teams are still choosing self-hosted solutions despite the higher price tag:</p><p><strong>🔒 Absolute Code Privacy</strong><br>Your proprietary algorithms, business logic, and intellectual property never leave your infrastructure. For companies in regulated industries or with highly sensitive codebases, this alone justifies the premium.</p><p><strong>🎯 Unlimited Customization</strong></p><ul><li>Fine-tune on your specific codebase and coding patterns</li><li>Integrate with internal tools and documentation</li><li>Customize response formats and behavior</li><li>No feature limitations or API rate limits</li></ul><p><strong>📈 Performance Predictability</strong></p><ul><li>Guaranteed latency and availability (no external dependencies)</li><li>Scale resources based on your team’s actual usage patterns</li><li>No “fair use” policies or throttling during peak development periods</li></ul><p><strong>🚫 Zero Vendor Risk</strong></p><ul><li>No risk of pricing changes, service discontinuation, or policy modifications</li><li>Complete control over model updates and feature rollouts</li><li>Independence from external service availability</li></ul><h4>Right-Sizing Your Infrastructure</h4><p><strong>For Teams Under 25 Developers</strong>: Consider sticking with commercial solutions unless you have specific privacy requirements. The infrastructure overhead isn’t justified.</p><p><strong>For Teams 25–100 Developers</strong>:</p><ul><li><strong>Entry Level</strong>: 1x H100 with Qwen2.5-Coder-32B</li><li><strong>Production</strong>: 2–3x H100 cluster with load balancing</li><li><strong>Enterprise</strong>: 4+ H100s with auto-scaling</li></ul><p><strong>For Teams 100+ Developers</strong>:</p><ul><li>Multi-GPU clusters become cost-competitive</li><li>Advanced features like model serving, A/B testing, and custom fine-tuning provide significant value</li></ul><h4>Performance Reality Check</h4><p>Let’s set realistic expectations about model performance compared to commercial solutions:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Mjml8ET5_pImIlP3yd7tLw.png" /><figcaption><strong>Bottom Line</strong>: The 32B model gets you into competitive territory, while the 7B model is better suited for experimentation and cost-conscious deployments where “good enough” code assistance is acceptable.</figcaption></figure><h4>GPU Requirements for Production Scale</h4><p><strong>Single H100 (94GB VRAM)</strong>:</p><ul><li>Runs Qwen2.5-Coder-32B comfortably</li><li>Supports 2–4 concurrent users with good latency</li><li>Suitable for small teams or development/testing</li></ul><p><strong>3x H100 Cluster</strong>:</p><ul><li>Load-balanced inference serving</li><li>Supports 50–100 concurrent users</li><li>Redundancy for high availability</li><li>Room for model updates without downtime</li></ul><p><strong>6+ H100 Cluster</strong>:</p><ul><li>Enterprise-grade performance</li><li>200+ concurrent users</li><li>Multiple model variants (A/B testing)</li><li>Custom fine-tuning capabilities</li></ul><h4>When Self-Hosting Makes Sense</h4><p>Self-hosting becomes strategically valuable when you can answer “yes” to multiple questions:</p><p>✅ <strong>Privacy Critical</strong>: Do you work with highly sensitive or regulated code?<br>✅ <strong>Scale Justified</strong>: Do you have 25+ developers who would use this daily?<br>✅ <strong>Customization Needed</strong>: Would model fine-tuning on your codebase provide significant value?<br>✅ <strong>Infrastructure Capability</strong>: Do you have ML/DevOps expertise to manage GPU infrastructure?<br>✅ <strong>Long-term View</strong>: Are you building AI capabilities as a core competency?</p><h4>The Strategic Investment Perspective</h4><p>Think of self-hosted coding assistants not as a cost-saving measure, but as a <strong>strategic infrastructure investment</strong>:</p><ul><li><strong>Data Sovereignty</strong>: Complete control over your intellectual property</li><li><strong>Competitive Advantage</strong>: Custom models trained on your specific domain</li><li><strong>Future-Proofing</strong>: Independence from external vendor decisions</li><li><strong>Team Capability</strong>: Building internal AI/ML expertise and infrastructure</li></ul><p>The premium you pay for self-hosting buys you something commercial solutions can’t offer: <strong>complete control over one of your most critical development tools</strong>.</p><p><em>In Part 2, we’ll dive deep into the performance benchmarking that justifies these infrastructure investments, showing you exactly what performance improvements you can expect from different GPU configurations and model sizes.</em></p><h3>Enter Qwen2.5-Coder: Purpose-Built for Code Generation</h3><p>While the benefits of self-hosting are clear, success depends on choosing the right model. This is where Qwen2.5-Coder shines. Unlike general-purpose language models adapted for coding, Qwen2.5-Coder was purpose-built from the ground up for software development tasks.</p><h4>What Makes Qwen2.5-Coder Special</h4><p><strong>🧠 Specialized Training</strong><br>Trained on 5.5 trillion tokens of curated code and technical documentation across 40+ programming languages, including Python, JavaScript, Java, C++, Go, Rust, and more.</p><p><strong>⚡ Optimized Architecture</strong><br>Designed specifically for the unique patterns of code generation — understanding syntax, semantics, dependencies, and project structure better than general-purpose models.</p><p><strong>🎯 Multi-Task Excellence</strong><br>Excels across the full spectrum of coding tasks:</p><ul><li>Code completion and generation</li><li>Bug detection and debugging</li><li>Code explanation and documentation</li><li>Refactoring and optimization suggestions</li><li>Cross-language translation</li></ul><h4>Qwen Model Family Overview</h4><p>The Qwen ecosystem offers multiple generations optimized for different use cases:</p><h4>Qwen2.5-Coder Series (Production-Ready)</h4><p><a href="https://huggingface.co/collections/Qwen/qwen25-coder-66eaa22e6f99801bf65b0c2f">Qwen2.5-Coder - a Qwen Collection</a></p><p>Qwen2.5-Coder offers six model sizes (0.5B, 1.5B, 3B, 7B, 14B, 32B) with significant improvements in code generation, code reasoning, and code fixing. The 32B model achieves competitive performance with GPT-4o.</p><ul><li>✅ <strong>Production-Ready</strong>: Mature, well-tested, extensive community adoption</li><li>✅ <strong>Proven Performance</strong>: Qwen2.5-Coder-32B-Instruct achieves state-of-the-art performance among open-source models on multiple coding benchmarks, with competitive performance against GPT-4o</li><li>✅ <strong>Diverse Model Sizes</strong>: Six variants covering all deployment scenarios</li><li>✅ <strong>Apache 2.0 License</strong>: Commercial-friendly licensing</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CjmiJeYWqt_B3CAOr8CHwQ.png" /><figcaption>Qwen2.5-Coder Series</figcaption></figure><h4>Qwen3 Series (Next-Generation Models with Hybrid Reasoning)</h4><p><a href="https://huggingface.co/collections/Qwen/qwen3-67dd247413f0e2e4f653967f">Qwen3 - a Qwen Collection</a></p><p>Qwen3 introduces “hybrid thinking” models capable of step-by-step reasoning that can be enabled or disabled, with models ranging from 0.6B to 235B parameters.</p><ul><li>🧠 <strong>Hybrid Reasoning</strong>: Can switch between “thinking” mode for deep reasoning and “non-thinking” mode for fast responses</li><li>📏 <strong>Extended Context</strong>: Up to 131K tokens (vs 32K in Qwen2.5)</li><li>⚡ <strong>MoE Efficiency</strong>: MoE models activate only a few “experts” per token, delivering high performance with lower inference costs</li><li>🌐 <strong>119 Languages</strong>: Enhanced multilingual capabilities</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-KhGFJ0H1uF_Ba9oWPnFqQ.png" /><figcaption>Qwen3 Series</figcaption></figure><h4>Qwen3-Coder Series (Specialized Agentic Coding Models)</h4><p><a href="https://huggingface.co/collections/Qwen/qwen3-coder-687fc861e53c939e52d52d10">Qwen3-Coder - a Qwen Collection</a></p><p>Qwen3-Coder represents the most agentic code model to date, with the flagship 480B-parameter model setting new state-of-the-art results among open models on agentic coding tasks.</p><ul><li>🤖 <strong>Agentic Design</strong>: Built for autonomous interaction with tools and developer environments</li><li>📊 <strong>Repository-Scale</strong>: Native 256K context support, extendable to 1M tokens for entire codebase analysis</li><li>🔧 <strong>Advanced Tool Calling</strong>: Seamless integration with development workflows and CI/CD pipelines</li><li>📈 <strong>SWE-Bench Performance</strong>: Achieves state-of-the-art results among open models on real-world software engineering tasks</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Db6xMAtAwdFz0gxCSbhNMA.png" /><figcaption>Qwen3-Coder Series</figcaption></figure><h4>Benchmark Performance Comparison</h4><ul><li><a href="https://qwenlm.github.io/blog/qwen2.5-coder-family/">Qwen2.5-Coder Series: Powerful, Diverse, Practical.</a></li><li><a href="https://qwenlm.github.io/blog/qwen3/">Qwen3: Think Deeper, Act Faster</a></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6jQDADsq0K4MyOxEgT5FNg.png" /><figcaption>Direct Performance Comparison: Qwen2.5 coder vs Commercial Models</figcaption></figure><h3>GPU Selection Strategy: Matching Hardware to Your Needs</h3><p>Choosing the right GPU is crucial for cost-effective deployment. Let’s break down your options:</p><h4>Understanding Key Technologies</h4><p>Before examining specific hardware, it’s important to understand the technologies that impact LLM performance:</p><p><strong>Compute Capability</strong>: NVIDIA’s versioning system indicating supported CUDA features:</p><ul><li>6.x (Pascal): Legacy, lacks tensor cores</li><li>7.x (Volta/Turing): First-gen tensor cores</li><li>8.x (Ampere): Excellent LLM support with 2nd/3rd-gen tensor cores</li><li>9.x (Hopper): Cutting-edge with FP8 support</li></ul><p><strong>FlashAttention 2</strong>: Memory-efficient attention algorithm that:</p><ul><li>Reduces VRAM usage from O(N²) to O(N)</li><li>Enables 32K+ token sequences</li><li>Provides 2–4x faster attention computation</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*k18PSB38vqA0p3aqtRMoZQ.png" /><figcaption>Comprehensive GPU Comparison for LLM Workloads</figcaption></figure><h3>Setting Up Your Development Environment with Azure ML</h3><p>Before diving into production deployment, we’ll start with experimentation in a controlled environment. Azure Machine Learning provides an ideal platform for this initial phase, offering managed Jupyter notebooks with GPU acceleration and seamless integration with cloud resources.</p><h4>Setting Up Azure ML Workspace</h4><p>Azure ML workspace serves as the central hub for our machine learning operations, providing compute management, model registry, and experiment tracking capabilities. We’ll create a workspace optimized for GPU workloads.</p><h4>Prerequisites</h4><p>Ensure you have the following installed and configured:</p><ul><li>Azure CLI (version 2.15.0 or later)</li><li>Python 3.8+ with pip</li><li>An active Azure subscription with sufficient quota for GPU compute</li></ul><h4>Creating Azure ML Resources</h4><p>First, let’s create the foundational Azure resources. Execute these commands in your Azure CLI:</p><pre># Set up variables for consistent naming<br>export RESOURCE_GROUP=&quot;rg-llm-deployment&quot;<br>export LOCATION=&quot;eastus2&quot;  # Choose a region with diverse GPU availability<br>export WORKSPACE_NAME=&quot;mlw-qwen-deployment&quot;<br>export COMPUTE_NAME_T4=&quot;gpu-compute-t4&quot;<br>export COMPUTE_NAME_H100=&quot;gpu-compute-h100&quot;<br><br># Create resource group<br>az group create \<br>    --name $RESOURCE_GROUP \<br>    --location $LOCATION<br># Create Azure ML workspace<br>az ml workspace create \<br>    --name $WORKSPACE_NAME \<br>    --resource-group $RESOURCE_GROUP \<br>    --location $LOCATION \<br>    --display-name &quot;Qwen LLM Multi-Model Deployment Workspace&quot; \<br>    --description &quot;Workspace for benchmarking Qwen model variants across GPU architectures&quot;<br># Configure default workspace for subsequent commands<br>az configure --defaults group=$RESOURCE_GROUP workspace=$WORKSPACE_NAME</pre><p>This screenshot shows the <strong>Azure Resource Group named </strong><strong>rg-llm-deployment</strong> containing the core services provisioned automatically when you create an Azure Machine Learning Workspace (in this case, named mlw‑qwen‑deployment)</p><p>The list of resources includes:</p><ul><li><strong>Application Insights</strong> (for monitoring deployed endpoints),</li><li><strong>Storage account</strong> (for storing datasets, notebooks, logs, and artifacts),</li><li><strong>Key Vault</strong> (for managing secrets),</li><li><strong>Log Analytics workspace</strong>,</li><li>and the <strong>Workspace itself</strong> — all created in the same region (East US 2) to support the ML studio.</li></ul><p>In summary: when you create a new Azure Machine Learning workspace, Azure automatically provisions all necessary dependent services within a single resource group so that everything needed to run experiments, track models, and deploy inference endpoints is available without additional setup.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YNFTQ-czLlMpa86qB9S7mw.png" /><figcaption>Azure Machine Learning workspace Resources</figcaption></figure><h4>Creating Multi-GPU Compute Instances</h4><p>We’ll create two compute instances to enable comprehensive benchmarking:</p><p><strong>T4 Compute Instance for Initial Exploration:</strong></p><pre># Create T4 compute instance for 7B model testing<br>az ml compute create \<br>    --name $COMPUTE_NAME_T4 \<br>    --type ComputeInstance \<br>    --size Standard_NC4as_T4_v3 \<br>    --enable-node-public-ip true \<br>    --description &quot;T4 GPU compute for Qwen 7B model experimentation&quot;</pre><p><strong>H100 NVL Compute Instance for Advanced Benchmarking:</strong></p><pre># Create H100 compute instance for 32B model testing and performance comparison<br>az ml compute create \<br>    --name $COMPUTE_NAME_H100 \<br>    --type ComputeInstance \<br>    --size Standard_NC40ads_H100_v5 \<br>    --enable-node-public-ip true \<br>    --description &quot;H100 NVL GPU compute for Qwen 32B model and performance benchmarking&quot;</pre><p><strong>Compute Instance Specifications:</strong></p><p><strong><em>Standard_NC4as_T4_v3 (T4 Instance):</em></strong></p><ul><li>1x NVIDIA T4 GPU (16 GB VRAM)</li><li>4 vCPUs</li><li>28 GB RAM</li><li>Premium SSD storage</li></ul><p><strong><em>Standard_NC40ads_H100_v5 (H100 Instance):</em></strong></p><ul><li>NVIDIA H100 NVL GPUs (94 GB VRAM)</li><li>40 vCPUs</li><li>320 GB RAM</li><li>Premium SSD storage</li></ul><p><strong>Note</strong>: H100 instances require significant quota increases. Submit quota requests well in advance, as approval can take 3–5 business days.</p><h4>Alternative: Using Azure ML Studio</h4><p>If you prefer the web interface, you can create these resources through Azure ML Studio:</p><ol><li>Navigate to <a href="https://ml.azure.com/">Azure ML Studio</a></li><li>Create a new workspace with the same specifications</li><li>Go to <strong>Compute</strong> → <strong>Compute instances</strong> → <strong>New</strong></li><li>Select <strong>Standard_NC4as_T4_v3</strong> as the VM size</li><li>Configure auto-shutdown to minimize costs</li></ol><h3>Configuring the Development Environment</h3><p>Once your compute instance is running, access the Jupyter environment by clicking on VS Code (Desktop) and set up the required dependencies.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fOyvKQ5Rm3lmcIhQYllGyg.png" /></figure><p>To use this, make sure the following <strong>VS Code Extensions are installed.</strong></p><ul><li>Azure Machine Learning</li><li>Python</li><li>Jupyter</li></ul><p>This will allow us to work on the Azure ML compute instance as if it were our local environment through VS Code.</p><p>Once you’ve accessed the compute instance via VS Code, you can open the terminal and run nvidia-smi to verify that the GPU resources are correctly configured.</p><p>Please note that, by default, the Azure compute quota for the Standard_NC4as instance family is set to zero. You will need to submit a quota increase request to Azure Support to obtain additional quota.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6S9JzqBGBq33aUuN3K_v-Q.png" /></figure><p>Azure ML compute instances come with several pre-configured conda environments, but these often have version conflicts for modern LLM workflows. The best approach is to create a dedicated environment with the exact package versions we need.</p><h4>Understanding Pre-installed Environments</h4><p>First, let’s examine what’s available on your compute instance:</p><pre># Connect to your compute instance via SSH or VS Code<br>conda env list</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x-deH_qFESqEa7PaCPVYCA.png" /></figure><p>You’ll see environments like:</p><ul><li>azureml_py38_PT_TF - PyTorch + TensorFlow (often has version conflicts)</li><li>azureml_py310_sdkv2 - Python 3.10 with Azure ML SDK v2</li><li>azureml_py38 - Basic Python 3.8 environment</li></ul><h4>Creating a Clean LLM Environment</h4><p>Instead of using these pre-configured environments, we’ll create a fresh one optimized for LLM development:</p><pre># Deactivate current environment and go to base<br>conda deactivate<br><br># Accept Terms of Service for Required Channels<br>conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main<br>conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r<br># Create new conda environment with Python 3.10<br>conda create -n qwen-llm python=3.10 -y<br># Activate the environment<br>conda activate qwen-llm<br># Install PyTorch with CUDA 12.1 (compatible with CUDA 12.2 drivers)<br>pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121<br># Install Flash Attention first (critical for clean compilation)<br>pip install flash-attn --no-build-isolation<br># Install core LLM packages with latest compatible versions<br>pip install transformers==4.54.1  # Latest stable version<br>pip install accelerate==0.33.0<br>pip install bitsandbytes==0.43.3<br>pip install sentencepiece==0.2.0<br># Install API server dependencies<br>pip install fastapi==0.104.1<br>pip install uvicorn[standard]==0.24.0<br>pip install python-multipart==0.0.6<br># Install data science and utility packages<br>pip install numpy pandas matplotlib seaborn<br>pip install azure-ai-ml azure-identity<br>pip install GPUtil psutil<br># Install additional performance packages<br>pip install einops  # Required by Flash Attention<br>pip install ninja   # Faster compilation<br><br># Verify PyTorch installation<br>python -c &quot;<br>import torch<br>print(f&#39;✅ PyTorch version: {torch.__version__}&#39;)<br>print(f&#39;✅ CUDA available: {torch.cuda.is_available()}&#39;)<br>print(f&#39;✅ GPU count: {torch.cuda.device_count()}&#39;)<br>if torch.cuda.is_available():<br>    print(f&#39;✅ GPU name: {torch.cuda.get_device_name(0)}&#39;)<br>    print(f&#39;✅ CUDA version: {torch.version.cuda}&#39;)<br>&quot;<br># Verify Flash Attention installation<br>python -c &quot;<br>try:<br>    import flash_attn<br>    print(&#39;⚡ Flash Attention installed successfully!&#39;)<br>    print(f&#39;⚡ Flash Attention version: {flash_attn.__version__}&#39;)<br>except ImportError as e:<br>    print(f&#39;❌ Flash Attention installation failed: {e}&#39;)<br>&quot;<br># Verify Transformers installation<br>python -c &quot;<br>import transformers<br>print(f&#39;🤖 Transformers version: {transformers.__version__}&#39;)<br>&quot;</pre><p><strong>Set Python Interpreter in VS code.</strong></p><ul><li>Open Command Palette (Ctrl+Shift+P)</li><li>Type “Python: Select Interpreter”</li><li>Choose the path: /anaconda/envs/qwen-llm/bin/python</li></ul><p><strong>Creating Jupyter Kernel for the Environment</strong></p><p>Register the new environment as a Jupyter kernel for notebook development:</p><pre># Install ipykernel in the qwen-llm environment<br>conda activate qwen-llm<br>pip install ipykernel<br><br># Register as Jupyter kernel<br>python -m ipykernel install --user --name qwen-llm --display-name &quot;Qwen LLM (Python 3.10)&quot;<br># Verify kernel installation<br>jupyter kernelspec list</pre><p>Now when you create notebooks in Azure ML Studio or VS Code, you can select the “Qwen LLM (Python 3.10)” kernel, ensuring consistent package versions across your development workflow.</p><h3>Environment Verification</h3><p>Create a new Jupyter notebook named <strong><em>qwen_exploration.ipynb</em></strong> in VS Code. Ensure you select the “Qwen LLM (Python 3.10)” kernel. Start by examining the available resources and verifying our environment setup:</p><pre>import torch<br>from transformers import AutoTokenizer, AutoModelForCausalLM<br>import time<br>import psutil<br>import sys<br>import os<br>import json<br>from datetime import datetime<br>import matplotlib.pyplot as plt<br>import pandas as pd<br><br># Verify environment setup<br>print(f&quot;🐍 Python executable: {sys.executable}&quot;)<br>print(f&quot;🔥 PyTorch version: {torch.__version__}&quot;)<br>print(f&quot;🤖 Transformers version: {__import__(&#39;transformers&#39;).__version__}&quot;)<br>print(f&quot;📅 Benchmark timestamp: {datetime.now().isoformat()}&quot;)<br>print(&quot;=&quot; * 60)<br>def get_system_info():<br>    &quot;&quot;&quot;Comprehensive system resource assessment&quot;&quot;&quot;<br>    info = {<br>        &quot;cpu_cores&quot;: psutil.cpu_count(),<br>        &quot;total_ram_gb&quot;: psutil.virtual_memory().total / (1024**3),<br>        &quot;available_ram_gb&quot;: psutil.virtual_memory().available / (1024**3),<br>        &quot;timestamp&quot;: datetime.now().isoformat()<br>    }<br>    <br>    if torch.cuda.is_available():<br>        info[&quot;cuda_version&quot;] = torch.version.cuda<br>        info[&quot;gpu_count&quot;] = torch.cuda.device_count()<br>        info[&quot;gpus&quot;] = []<br>        <br>        for i in range(torch.cuda.device_count()):<br>            gpu_props = torch.cuda.get_device_properties(i)<br>            gpu_info = {<br>                &quot;id&quot;: i,<br>                &quot;name&quot;: gpu_props.name,<br>                &quot;memory_gb&quot;: gpu_props.total_memory / (1024**3),<br>                &quot;compute_capability&quot;: f&quot;{gpu_props.major}.{gpu_props.minor}&quot;,<br>                &quot;multiprocessor_count&quot;: gpu_props.multi_processor_count<br>            }<br>            info[&quot;gpus&quot;].append(gpu_info)<br>            print(f&quot;🎮 GPU {i}: {gpu_props.name}&quot;)<br>            print(f&quot;   💾 Memory: {gpu_props.total_memory / (1024**3):.2f} GB&quot;)<br>            print(f&quot;   🔧 Compute capability: {gpu_props.major}.{gpu_props.minor}&quot;)<br>    else:<br>        print(&quot;❌ CUDA not available - check GPU drivers and PyTorch installation&quot;)<br>        return None<br>    <br>    print(f&quot;🖥️  CPU cores: {info[&#39;cpu_cores&#39;]}&quot;)<br>    print(f&quot;🧠 Total RAM: {info[&#39;total_ram_gb&#39;]:.2f} GB&quot;)<br>    print(f&quot;💚 Available RAM: {info[&#39;available_ram_gb&#39;]:.2f} GB&quot;)<br>    <br>    return info<br># Get and store system information<br>system_info = get_system_info()<br>if system_info is None:<br>    print(&quot;\n🚨 GPU setup required before continuing. Check environment configuration.&quot;)<br>    exit()</pre><pre>/anaconda/envs/qwen-llm/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html<br>  from .autonotebook import tqdm as notebook_tqdm<br>🐍 Python executable: /anaconda/envs/qwen-llm/bin/python<br>🔥 PyTorch version: 2.7.1+cu118<br>🤖 Transformers version: 4.44.0<br>📅 Benchmark timestamp: 2025-08-02T14:31:51.462687<br>============================================================<br>🎮 GPU 0: NVIDIA H100 NVL<br>   💾 Memory: 93.02 GB<br>   🔧 Compute capability: 9.0<br>🖥️  CPU cores: 40<br>🧠 Total RAM: 314.69 GB<br>💚 Available RAM: 310.76 GB</pre><h4>Troubleshooting Common Issues</h4><p>If you encounter CUDA-related errors, verify these common issues:</p><pre># Common troubleshooting checks<br>def troubleshoot_setup():<br>    print(&quot;=== Troubleshooting GPU Setup ===&quot;)<br>    <br>    # Check CUDA installation<br>    try:<br>        import torch<br>        print(f&quot;✅ PyTorch imported successfully&quot;)<br>        print(f&quot;   CUDA compiled version: {torch.version.cuda}&quot;)<br>        print(f&quot;   CUDA runtime version: {torch.cuda.get_device_capability()}&quot;)<br>    except Exception as e:<br>        print(f&quot;❌ PyTorch import error: {e}&quot;)<br>        return False<br>    <br>    # Check GPU accessibility<br>    if torch.cuda.is_available():<br>        device = torch.cuda.current_device()<br>        print(f&quot;✅ GPU accessible: {torch.cuda.get_device_name(device)}&quot;)<br>        <br>        # Test basic tensor operations<br>        try:<br>            test_tensor = torch.randn(1000, 1000).cuda()<br>            result = torch.mm(test_tensor, test_tensor.t())<br>            print(f&quot;✅ GPU tensor operations working&quot;)<br>            del test_tensor, result  # Free memory<br>            torch.cuda.empty_cache()<br>        except Exception as e:<br>            print(f&quot;❌ GPU tensor operations failed: {e}&quot;)<br>            return False<br>    else:<br>        print(&quot;❌ CUDA not available&quot;)<br>        return False<br>    <br>    return True<br><br># Run troubleshooting if needed<br>troubleshoot_setup()</pre><pre>=== Troubleshooting GPU Setup ===<br>✅ PyTorch imported successfully<br>   CUDA compiled version: 11.8<br>   CUDA runtime version: (9, 0)<br>✅ GPU accessible: NVIDIA H100 NVL<br>✅ GPU tensor operations working<br>True</pre><p>This diagnostic approach helps identify environment issues early, preventing frustrating model loading failures later in the process.</p><h3>Loading Your First Qwen Model</h3><p>Now for the exciting part — loading and testing Qwen2.5-Coder-7B-Instruct:</p><p>Since the NVIDIA T4 has 16GB of VRAM, we need to be strategic about model loading. Qwen2.5-Coder-7B-Instruct in FP16 precision requires approximately 14GB of VRAM, leaving us with a comfortable margin for inference operations:</p><pre># Load model with memory optimization<br>model_name = &quot;Qwen/Qwen2.5-Coder-7B-Instruct&quot;<br><br># Load tokenizer<br>tokenizer = AutoTokenizer.from_pretrained(model_name)<br># Load model with optimizations for T4 constraints<br>model = AutoModelForCausalLM.from_pretrained(<br>    model_name,<br>    torch_dtype=torch.float16,  # Use FP16 for memory efficiency<br>    device_map=&quot;auto&quot;,          # Automatic device placement<br>    trust_remote_code=True,     # Required for Qwen models<br>    low_cpu_mem_usage=True      # Optimize CPU memory during loading<br>)<br># Move to GPU and optimize<br>model = model.eval()  # Set to evaluation mode<br>print(f&quot;Model loaded on device: {model.device}&quot;)<br>print(f&quot;Model memory footprint: {model.get_memory_footprint() / 1e9:.2f} GB&quot;)</pre><pre>Downloading shards: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [06:24&lt;00:00, 96.08s/it]<br>Loading checkpoint shards: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [02:40&lt;00:00, 40.17s/it]<br>Some parameters are on the meta device device because they were offloaded to the cpu.<br>✅ Model loaded successfully!<br>📍 Device: cuda:0<br>💾 Model memory: 15.70 GB<br>🔥 GPU Memory - Allocated: 13.73 GB, Reserved: 13.96 GB</pre><blockquote>Now, by using the <em>nvidia-smi</em> command, we can confirm that the model has been loaded onto the GPU by checking the GPU memory utilization, which shows <strong>13,431 MiB / 16,384 MiB</strong>.</blockquote><pre>(qwen-llm) azureuser@gpu-compute-t4:~/cloudfiles/code$ nvidia-smi<br>Fri Aug  1 16:51:10 2025       <br>+---------------------------------------------------------------------------------------+<br>| NVIDIA-SMI 535.247.01             Driver Version: 535.247.01   CUDA Version: 12.2     |<br>|-----------------------------------------+----------------------+----------------------+<br>| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |<br>| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |<br>|                                         |                      |               MIG M. |<br>|=========================================+======================+======================|<br>|   0  Tesla T4                       On  | 00000001:00:00.0 Off |                  Off |<br>| N/A   50C    P0              30W /  70W |  13431MiB / 16384MiB |      0%      Default |<br>|                                         |                      |                  N/A |<br>+-----------------------------------------+----------------------+----------------------+<br>                                                                                         <br>+---------------------------------------------------------------------------------------+<br>| Processes:                                                                            |<br>|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |<br>|        ID   ID                                                             Usage      |<br>|=======================================================================================|<br>|    0   N/A  N/A     21916      C   /anaconda/envs/qwen-llm/bin/python        13426MiB |<br>+---------------------------------------------------------------------------------------+<br>(qwen-llm) azureuser@gpu-compute-t4:~/cloudfiles/code$</pre><p>This setup provides the foundation for our production deployment while allowing us to experiment with different configurations and understand the model’s behavior in a controlled environment.</p><h3>Your First Code Generation</h3><p>Let’s test the model with simple text generation to ensure everything works:</p><pre>def generate_text(prompt, max_length=200, temperature=0.7, top_p=0.8):<br>    &quot;&quot;&quot;Basic text generation function&quot;&quot;&quot;<br>    <br>    # Encode input<br>    inputs = tokenizer(prompt, return_tensors=&quot;pt&quot;, padding=True).to(model.device)<br>    <br>    # Track GPU memory before generation<br>    memory_before = torch.cuda.memory_allocated() / 1e9<br>    start_time = time.time()<br>    <br>    # Generate response<br>    with torch.no_grad():<br>        outputs = model.generate(<br>            input_ids=inputs[&quot;input_ids&quot;],<br>            attention_mask=inputs[&quot;attention_mask&quot;],<br>            max_length=max_length,<br>            temperature=temperature,<br>            top_p=top_p,<br>            do_sample=True,<br>            pad_token_id=tokenizer.eos_token_id,<br>            eos_token_id=tokenizer.eos_token_id<br>        )<br>    <br>    # Calculate metrics<br>    generation_time = time.time() - start_time<br>    memory_after = torch.cuda.memory_allocated() / 1e9<br>    <br>    # Decode response<br>    response = tokenizer.decode(outputs[0], skip_special_tokens=True)<br>    new_text = response[len(prompt):].strip()<br>    <br>    print(f&quot;🕒 Generation time: {generation_time:.2f}s&quot;)<br>    print(f&quot;💾 Memory delta: {memory_after - memory_before:.3f} GB&quot;)<br>    print(f&quot;📝 Generated tokens: {len(outputs[0]) - len(inputs[0])}&quot;)<br>    print(f&quot;⚡ Tokens/second: {(len(outputs[0]) - len(inputs[0])) / generation_time:.1f}&quot;)<br>    <br>    return new_text<br><br><br># Test basic generation<br>test_prompt = &quot;Explain the benefits of using Kubernetes for machine learning workloads:&quot;<br>result = generate_text(test_prompt, max_length=2000)<br>print(f&quot;\n📄 Generated text:\n{result}&quot;)</pre><pre>🕒 Generation time: 112.13s<br>💾 Memory delta: 0.000 GB<br>📝 Generated tokens: 428<br>⚡ Tokens/second: 3.8<br>📄 Generated text:<br>1. Scalability and Flexibility: Kubernetes allows you to easily scale your ML models up or down as needed, based on demand. This means that you can handle sudden spikes in traffic without compromising performance. Additionally, Kubernetes provides a high level of flexibility, allowing you to run different types of workloads (e.g., batch processing, real-time inference) on the same cluster.<br>2. Automation and Orchestration: Kubernetes automates many of the tasks involved in managing ML workloads, such as deploying, scaling, and updating models. This saves time and reduces the risk of human error. Additionally, Kubernetes provides built-in support for orchestration, which helps ensure that your ML models are always available and running smoothly.<br>3. Cost Efficiency: Kubernetes can help you save money by reducing the need for physical hardware. By using containerized ML models, you can run multiple instances of a model on a single machine, rather than requiring separate hardware for each instance. Additionally, Kubernetes provides a pay-as-you-go pricing model, which means that you only pay for the resources that you use.<br>4. Improved Security: Kubernetes provides a number of security features that can help protect your ML models from attacks. For example, it allows you to define access controls and enforce network policies, which helps limit the potential damage that an attacker could cause. Additionally, Kubernetes uses encryption to secure data in transit and at rest, which helps prevent unauthorized access.<br>5. Better Performance: Kubernetes can improve the performance of your ML models by providing a more efficient way to manage resources. For example, it allows you to allocate specific resources to each model, which ensures that they have the resources they need to perform well. Additionally, Kubernetes provides a number of tools and libraries that can help optimize the performance of your ML models, such as TensorFlow Serving and PyTorch Lightning.<br>Overall, Kubernetes offers a powerful set of features that can help you manage and deploy ML workloads more efficiently and effectively. Whether you&#39;re working with small-scale projects or large-scale enterprise applications, Kubernetes can provide significant benefits in terms of scalability, automation, cost efficiency, security, and performance.</pre><h3>✉️ Implementing Chat Completion with Qwen2.5-Instruct</h3><p>Qwen2.5-Instruct models use a structured token-based chat format (&lt;|im_start|&gt;, &lt;|im_end|&gt;) to delineate roles like system, user, and assistant. To enable rich multi-turn conversations with memory of previous context, we format the conversation history into a single prompt string using this template.</p><pre>def format_chat_prompt(messages):<br>    &quot;&quot;&quot;Format messages for Qwen2.5-Instruct chat template&quot;&quot;&quot;<br>    <br>    # Qwen2.5-Instruct uses a specific chat format<br>    formatted_prompt = &quot;&quot;<br>    <br>    for message in messages:<br>        role = message[&quot;role&quot;]<br>        content = message[&quot;content&quot;]<br>        <br>        if role == &quot;system&quot;:<br>            formatted_prompt += f&quot;&lt;|im_start|&gt;system\n{content}&lt;|im_end|&gt;\n&quot;<br>        elif role == &quot;user&quot;:<br>            formatted_prompt += f&quot;&lt;|im_start|&gt;user\n{content}&lt;|im_end|&gt;\n&quot;<br>        elif role == &quot;assistant&quot;:<br>            formatted_prompt += f&quot;&lt;|im_start|&gt;assistant\n{content}&lt;|im_end|&gt;\n&quot;<br>    <br>    # Add assistant start token for generation<br>    formatted_prompt += &quot;&lt;|im_start|&gt;assistant\n&quot;<br>    return formatted_prompt</pre><p>Once formatted, we use model.generate() to produce the assistant’s next reply. The chat_completion() function wraps this process with tokenization, generation, decoding, and detailed performance tracking—reporting tokens per second, memory usage, and inference time.</p><pre>def chat_completion(messages, max_tokens=500, temperature=0.7, top_p=0.8):<br>    &quot;&quot;&quot;OpenAI-style chat completion function&quot;&quot;&quot;<br>    <br>    # Format the conversation<br>    prompt = format_chat_prompt(messages)<br>    <br>    # Tokenize<br>    inputs = tokenizer.encode(prompt, return_tensors=&quot;pt&quot;).to(model.device)<br>    input_length = len(inputs[0])<br>    <br>    # Track performance metrics<br>    start_time = time.time()<br>    memory_before = torch.cuda.memory_allocated()<br>    <br>    # Generate response<br>    with torch.no_grad():<br>        outputs = model.generate(<br>            inputs,<br>            max_new_tokens=max_tokens,  # Use max_new_tokens instead of max_length<br>            temperature=temperature,<br>            top_p=top_p,<br>            do_sample=True,<br>            pad_token_id=tokenizer.eos_token_id,<br>            eos_token_id=tokenizer.eos_token_id,<br>            stop_strings=[&quot;&lt;|im_end|&gt;&quot;],  # Stop at chat format end token<br>            tokenizer=tokenizer<br>        )<br>    <br>    # Calculate metrics<br>    generation_time = time.time() - start_time<br>    memory_after = torch.cuda.memory_allocated()<br>    total_tokens = len(outputs[0])<br>    new_tokens = total_tokens - input_length<br>    <br>    # Decode and clean response<br>    full_response = tokenizer.decode(outputs[0], skip_special_tokens=True)<br>    assistant_response = full_response[len(tokenizer.decode(inputs[0], skip_special_tokens=True)):].strip()<br>    <br>    # Remove chat format tokens if present<br>    if assistant_response.endswith(&quot;&lt;|im_end|&gt;&quot;):<br>        assistant_response = assistant_response[:-11].strip()<br>    <br>    # Performance metrics<br>    tokens_per_second = new_tokens / generation_time if generation_time &gt; 0 else 0<br>    memory_used_mb = (memory_after - memory_before) / 1e6<br>    <br>    return {<br>        &quot;response&quot;: assistant_response,<br>        &quot;metrics&quot;: {<br>            &quot;generation_time&quot;: generation_time,<br>            &quot;input_tokens&quot;: input_length,<br>            &quot;output_tokens&quot;: new_tokens,<br>            &quot;total_tokens&quot;: total_tokens,<br>            &quot;tokens_per_second&quot;: tokens_per_second,<br>            &quot;memory_used_mb&quot;: memory_used_mb<br>        }<br>    }</pre><p>Here’s a typical interaction test:</p><pre># Test chat completion format<br>test_messages = [<br>    {<br>        &quot;role&quot;: &quot;system&quot;, <br>        &quot;content&quot;: &quot;You are a helpful AI assistant specializing in cloud infrastructure and DevOps.&quot;<br>    },<br>    {<br>        &quot;role&quot;: &quot;user&quot;, <br>        &quot;content&quot;: &quot;What are the key advantages of deploying LLMs on Kubernetes compared to traditional VM-based deployments?&quot;<br>    }<br>]<br><br>print(&quot;🧪 Testing chat completion format...&quot;)<br>result = chat_completion(test_messages, max_tokens=1000)<br>print(f&quot;\n📋 Performance Metrics:&quot;)<br>print(f&quot;   ⏱️  Generation time: {result[&#39;metrics&#39;][&#39;generation_time&#39;]:.2f}s&quot;)<br>print(f&quot;   📥 Input tokens: {result[&#39;metrics&#39;][&#39;input_tokens&#39;]}&quot;)<br>print(f&quot;   📤 Output tokens: {result[&#39;metrics&#39;][&#39;output_tokens&#39;]}&quot;)<br>print(f&quot;   ⚡ Speed: {result[&#39;metrics&#39;][&#39;tokens_per_second&#39;]:.1f} tokens/sec&quot;)<br>print(f&quot;   💾 Memory used: {result[&#39;metrics&#39;][&#39;memory_used_mb&#39;]:.1f} MB&quot;)<br>print(f&quot;\n🤖 Assistant Response:\n{result[&#39;response&#39;]}&quot;)</pre><p>The assistant returns an accurate, structured response while tracking:</p><ul><li>⏱️ Generation time (in seconds)</li><li>⚡ Tokens per second</li><li>💾 Memory used during inference</li></ul><pre>The attention mask is not set and cannot be inferred from input because pad token is same as eos token. As a consequence, you may observe unexpected behavior. Please pass your input&#39;s `attention_mask` to obtain reliable results.<br>🧪 Testing chat completion format...<br><br>📋 Performance Metrics:<br>   ⏱️  Generation time: 28.54s<br>   📥 Input tokens: 46<br>   📤 Output tokens: 680<br>   ⚡ Speed: 23.8 tokens/sec<br>   💾 Memory used: 33.6 MB<br>🤖 Assistant Response:<br>Deploying Large Language Models (LLMs) on Kubernetes offers several key advantages over traditional VM-based deployments, particularly in the context of scalability, efficiency, and operational flexibility. Here are some of the primary benefits:<br>1. **Scalability**:<br>   - **Horizontal Scaling**: Kubernetes allows for easy horizontal scaling of LLM workloads. You can scale up or down the number of pods (instances) running your model based on demand, which is crucial for handling variable loads typical in applications using LLMs.<br>   - **Auto-scaling**: With Kubernetes, you can set up auto-scaling policies that automatically adjust the number of replicas of your application based on CPU/memory usage or custom metrics, ensuring optimal resource utilization.<br>2. **Resource Efficiency**:<br>   - **Efficient Resource Allocation**: Kubernetes uses containers, which are lightweight and share the host OS kernel, allowing for more efficient use of resources compared to full VMs. This means you can run more instances of your LLM on the same hardware.<br>   - **Fine-grained Resource Management**: You can specify precise CPU and memory requirements for each container, optimizing resource allocation and reducing costs.<br>3. **Operational Flexibility**:<br>   - **Declarative Configuration**: Kubernetes uses declarative configuration files (YAML) to define the desired state of your application, making it easier to manage and replicate environments across different stages (development, testing, production).<br>   - **Continuous Integration/Continuous Deployment (CI/CD)**: Kubernetes integrates well with CI/CD pipelines, allowing for rapid deployment and updates of your LLM applications with minimal downtime.<br>4. **High Availability and Fault Tolerance**:<br>   - **Self-healing**: Kubernetes automatically restarts failed containers, replaces and reschedules them on healthy nodes, and kills containers that don&#39;t respond to user-defined health checks.<br>   - **Load Balancing**: Kubernetes provides built-in load balancing to distribute traffic across multiple instances of your application, improving availability and reliability.<br>5. **Security**:<br>   - **Isolation**: Containers provide better isolation between different applications and services, reducing the risk of security breaches.<br>   - **Network Policies**: Kubernetes supports network policies to control traffic flow at the IP address or port level, enhancing security.<br>6. **Monitoring and Logging**:<br>   - **Built-in Monitoring**: Tools like Prometheus and Grafana can be integrated with Kubernetes to monitor application performance and resource usage in real-time.<br>   - **Centralized Logging**: Kubernetes supports centralized logging solutions like ELK Stack or Fluentd, making it easier to aggregate and analyze logs from all containers.<br>7. **Cost-Effectiveness**:<br>   - **Pay-as-you-go Model**: By efficiently utilizing resources and scaling only when necessary, Kubernetes can help reduce cloud infrastructure costs.<br>   - **Spot Instances**: Kubernetes can be configured to use spot instances, which are significantly cheaper than on-demand instances, providing a cost-effective way to run LLMs.<br>8. **Ease of Use and Community Support**:<br>   - **Extensive Documentation and Community**: Kubernetes has extensive documentation and a large community, making it easier to find support and best practices for deploying and managing LLMs.<br>In summary, Kubernetes provides a robust, scalable, and flexible platform for deploying LLMs, offering significant advantages over traditional VM-based deployments in terms of resource management, operational efficiency, and cost-effectiveness.</pre><p>This makes it easy to monitor performance and adjust parameters for latency-sensitive use cases such as real-time chat or Slack integrations.</p><h3>What’s Next?</h3><p>You now have a working self-hosted coding assistant! In the remaining parts of this series, we’ll transform this proof-of-concept into a production-ready system:</p><blockquote><strong>Part 2</strong> will dive deep into performance benchmarking across different GPU architectures, helping you choose the optimal hardware configuration and model variant for your specific needs.</blockquote><blockquote><strong>Part 3</strong> will guide you through building a production-ready FastAPI server with OpenAI-compatible endpoints, proper error handling, and monitoring capabilities.</blockquote><blockquote><strong>Part 4</strong> will cover deploying your API on Kubernetes with NVIDIA L4 GPUs, implementing auto-scaling, monitoring, and integrating with VS Code for a seamless development experience.</blockquote><h3>Quick Start Summary</h3><p>Here’s what you’ve accomplished in this first part:</p><p>✅ <strong>Understanding</strong>: Learned why self-hosting coding assistants makes business sense<br>✅ <strong>Environment</strong>: Set up Azure ML workspace with GPU compute<br>✅ <strong>Model</strong>: Successfully loaded and tested Qwen2.5-Coder-7B-Instruct<br>✅ <strong>Generation</strong>: Implemented both completion and chat-style interactions</p><p>Your self-hosted coding assistant is already functional and can help with real development tasks. The foundation is set — now we’ll build upon it to create a robust, scalable solution that can serve your entire development team.</p><blockquote>Ready to optimize for maximum performance? Part 2 will show you how to benchmark different model configurations and squeeze every ounce of performance from your GPU investment.</blockquote><p><strong>Resources</strong>:</p><ul><li><a href="https://huggingface.co/collections/Qwen/qwen25-coder-66eaa22e6f99801bf65b0c2f">Qwen2.5-Coder Model Collection</a></li><li><a href="https://docs.microsoft.com/en-us/azure/machine-learning/how-to-manage-quotas">Azure ML GPU Quota Requests</a></li><li>Complete Code <a href="https://github.com/tharindu-sankalpa/huggingface-learning/blob/main/qwen_exploration.ipynb">Repository</a> <em>(Coming in Part 3)</em></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9c94153cc235" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From Hugging Face Fundamentals to Production: Deploying Vision Transformers for NSFW Detection]]></title>
            <link>https://medium.com/@wltsankalpa/from-hugging-face-fundamentals-to-production-deploying-vision-transformers-for-nsfw-detection-fa04de9e4a92?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/fa04de9e4a92</guid>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Sun, 20 Jul 2025 18:40:20 GMT</pubDate>
            <atom:updated>2025-07-20T18:40:20.008Z</atom:updated>
            <content:encoded><![CDATA[<p><em>A comprehensive journey from learning Hugging Face basics to building and deploying production-grade Vision Transformer APIs</em></p><h3>Introduction</h3><p>In the rapidly evolving landscape of artificial intelligence, Hugging Face has emerged as the de facto standard for machine learning practitioners worldwide. What started as a chatbot company has transformed into the GitHub of machine learning, democratizing access to state-of-the-art models and making advanced AI accessible to everyone.</p><p>This article takes you on a comprehensive journey from understanding the core concepts of Hugging Face to building a production-grade Vision Transformer (ViT) API for NSFW (Not Safe For Work) image detection. We’ll explore not just the theoretical foundations but also the practical challenges of taking Vision Transformer models from research to production deployment.</p><p>By the end of this article, you’ll understand:</p><ul><li>The Hugging Face ecosystem and its core components</li><li>How to work with Vision Transformer models, datasets, and the Hub</li><li>The art of converting ViT models for production optimization using ONNX</li><li>Building scalable Vision Transformer APIs with authentication, monitoring, and rate limiting</li><li>Environment setup using modern Python tooling (UV)</li><li>Real-world deployment of computer vision models in production systems</li></ul><h3>Part I: Understanding the Hugging Face Ecosystem</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ttCC8ujG81nz3SWtfHZcWQ.png" /><figcaption>Hugging Face Ecosystem</figcaption></figure><h3>What is Hugging Face?</h3><p><a href="https://huggingface.co/">Hugging Face - The AI community building the future.</a></p><p>Hugging Face is much more than a library — it’s an entire ecosystem that has revolutionized how we work with machine learning models. At its core, it provides:</p><ol><li><strong>Open-source libraries</strong> (Transformers, Datasets, Tokenizers)</li><li><strong>The Hub</strong> — A collaborative platform for sharing models and datasets</li><li><strong>Inference APIs</strong> and deployment solutions</li><li><strong>Educational resources</strong> and community support</li></ol><p>The company’s mission is to “democratize good machine learning,” and they’ve succeeded by making sophisticated AI models as easy to use as any Python package.</p><h3>The Transformers Library: Your Gateway to Modern AI</h3><p>The Transformers library is the crown jewel of Hugging Face’s offerings. It provides a unified API for working with transformer models across different frameworks (PyTorch, TensorFlow, JAX).</p><pre>from transformers import pipeline<br><br># This simple line gives you access to state-of-the-art models<br>classifier = pipeline(&quot;sentiment-analysis&quot;)<br>result = classifier(&quot;I love learning about AI!&quot;)<br>print(result)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3piE_8GhBphNa6shpK4lYg.png" /></figure><p>What makes this remarkable is that behind this simple interface lies a pre-trained BERT model, tokenization, post-processing, and error handling — all abstracted away for ease of use.</p><h3>The Magic of Pipelines</h3><p>Pipelines are Hugging Face’s high-level API that provides a simple interface to complex models. They handle the entire machine learning workflow:</p><ol><li><strong>Input preprocessing</strong> (tokenization, image resizing, etc.)</li><li><strong>Model inference</strong></li><li><strong>Output post-processing</strong></li></ol><p>Let’s explore the most common pipeline tasks:</p><h4>1. Sentiment Analysis</h4><pre>from transformers import pipeline<br><br>sentiment_pipeline = pipeline(&quot;sentiment-analysis&quot;)<br>texts = [<br>    &quot;I love this movie!&quot;,<br>    &quot;This is terrible.&quot;,<br>    &quot;It&#39;s okay, not great but not bad either.&quot;<br>]<br><br>results = sentiment_pipeline(texts)<br>for text, result in zip(texts, results):<br>    print(f&quot;Text: &#39;{text}&#39; -&gt; {result}&quot;)</pre><h4>2. Text Generation</h4><pre>generator = pipeline(&quot;text-generation&quot;, model=&quot;gpt2&quot;)<br>prompt = &quot;The future of artificial intelligence is&quot;<br>result = generator(prompt, max_length=50, num_return_sequences=1)<br>print(f&quot;Generated: {result[0][&#39;generated_text&#39;]}&quot;)</pre><h4>3. Question Answering</h4><pre>qa_pipeline = pipeline(&quot;question-answering&quot;)<br>context = &quot;Hugging Face is a company that develops tools for machine learning. They are known for their Transformers library.&quot;<br>question = &quot;What is Hugging Face known for?&quot;<br><br>answer = qa_pipeline(question=question, context=context)<br>print(f&quot;Answer: {answer[&#39;answer&#39;]} (confidence: {answer[&#39;score&#39;]:.4f})&quot;)</pre><h4>4. Named Entity Recognition</h4><pre>ner_pipeline = pipeline(&quot;ner&quot;, aggregation_strategy=&quot;simple&quot;)<br>text = &quot;Apple Inc. was founded by Steve Jobs in Cupertino, California.&quot;<br>entities = ner_pipeline(text)<br><br>for entity in entities:<br>    print(f&quot;Entity: &#39;{entity[&#39;word&#39;]}&#39; - Type: {entity[&#39;entity_group&#39;]}&quot;)for entity in entities:<br>    print(f&quot;Entity: &#39;{entity[&#39;word&#39;]}&#39; - Type: {entity[&#39;entity_group&#39;]}&quot;)</pre><h3>The Hugging Face Hub: The GitHub of Machine Learning</h3><p>The Hugging Face Hub (<a href="https://huggingface.co/">https://huggingface.co</a>) is a collaborative platform hosting over 500,000 models, 100,000 datasets, and thousands of ML applications. It’s where the magic of open-source AI truly shines.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_rlGduUZ-8kOM5wpDCVI6Q.png" /></figure><h4>Key Features of the Hub:</h4><ul><li><strong>Version control</strong> for models and datasets using Git LFS</li><li><strong>Model cards</strong> with detailed documentation</li><li><strong>Interactive demos</strong> to test models in your browser</li><li><strong>Community discussions</strong> and collaboration tools</li><li><strong>Organizations</strong> for team collaboration</li></ul><h4>Understanding Model Repositories</h4><p>Each model on the Hub is a Git repository containing:</p><ul><li>Model weights (usually in SafeTensors format)</li><li>Configuration files (config.json)</li><li>Tokenizer files</li><li>README with model documentation</li><li>Optional demo applications</li></ul><h3>Models, Checkpoints, and the Auto Classes</h3><h4>Models vs Checkpoints</h4><ul><li><strong>Model</strong>: The neural network architecture (e.g., BERT, GPT, ViT)</li><li><strong>Checkpoint</strong>: A specific trained instance of a model with learned weights</li></ul><p>When you use a model from Hugging Face, you’re typically loading a checkpoint that has been pre-trained on large datasets.</p><h4>The Power of Auto Classes</h4><p>Auto classes are Hugging Face’s way of providing a unified interface across different model architectures:</p><pre>from transformers import (<br>    AutoTokenizer, <br>    AutoModel, <br>    AutoModelForSequenceClassification<br>)<br><br># These will automatically detect the correct model type<br>tokenizer = AutoTokenizer.from_pretrained(&quot;distilbert-base-uncased&quot;)<br>model = AutoModelForSequenceClassification.from_pretrained(&quot;distilbert-base-uncased&quot;)# These will automatically detect the correct model type<br>tokenizer = AutoTokenizer.from_pretrained(&quot;distilbert-base-uncased&quot;)<br>model = AutoModelForSequenceClassification.from_pretrained(&quot;distilbert-base-uncased&quot;)</pre><p>The beauty of Auto classes is that you can swap models without changing your code:</p><pre># Switch from DistilBERT to RoBERTa with zero code changes<br>tokenizer = AutoTokenizer.from_pretrained(&quot;roberta-base&quot;)<br>model = AutoModelForSequenceClassification.from_pretrained(&quot;roberta-base&quot;)</pre><h3>Working with Datasets</h3><p>The Datasets library provides efficient tools for working with machine learning datasets:</p><pre>from datasets import load_dataset<br><br># Load popular datasets with one line<br>dataset = load_dataset(&quot;imdb&quot;)<br>print(dataset)<br><br># Create your own dataset<br>from datasets import Dataset<br>import pandas as pd<br><br>df = pd.DataFrame({<br>    &quot;text&quot;: [&quot;I love this!&quot;, &quot;This is bad&quot;],<br>    &quot;label&quot;: [1, 0]<br>})<br>dataset = Dataset.from_pandas(df)</pre><h3>Environment Setup with UV: Modern Python Dependency Management</h3><p>Before diving into our production API, let’s discuss environment setup. We’ll use UV (Universal Virtualenv), a modern Python package manager that’s significantly faster than pip.</p><h4>Why UV?</h4><ul><li><strong>Speed</strong>: 10–100x faster than pip</li><li><strong>Reliability</strong>: Better dependency resolution</li><li><strong>Modern</strong>: Built for Python 3.8+</li><li><strong>Drop-in replacement</strong>: Works with existing requirements.txt</li></ul><h4>Setting up the environment:</h4><pre># Install UV<br>curl -LsSf https://astral.sh/uv/install.sh | sh<br><br># Create and activate environment<br>uv venv<br>source .venv/bin/activate<br><br># Install dependencies<br>uv sync</pre><p>Our project’s pyproject.toml includes all necessary dependencies:</p><pre>[project]<br>name = &quot;huggingface-learning&quot;<br>version = &quot;0.1.0&quot;<br>requires-python = &quot;&gt;=3.11&quot;<br>dependencies = [<br>    &quot;transformers&gt;=4.53.2&quot;,<br>    &quot;torch&gt;=2.7.1&quot;,<br>    &quot;fastapi&gt;=0.116.1&quot;,<br>    &quot;onnxruntime&gt;=1.22.1&quot;,<br>    &quot;prometheus-client&gt;=0.20.0&quot;,<br>    # ... other dependencies<br>]</pre><h3>The Use Case: NSFW Image Detection with Vision Transformers</h3><p>Before diving into the production implementation, let’s understand our specific use case and the technology powering it.</p><h3>What is NSFW Content Detection?</h3><p><strong>NSFW Image Classification</strong> refers to the process of automatically detecting and categorizing images that contain <strong>Not Safe For Work (NSFW)</strong> content — i.e., content that is inappropriate for workplace or public settings. This typically includes images that depict:</p><ul><li><strong>Nudity or pornography</strong></li><li><strong>Sexual activity or erotic content</strong></li><li><strong>Graphic violence or gore</strong> (in some models)</li><li><strong>Explicit or suggestive imagery</strong></li></ul><p>NSFW (Not Safe For Work) content detection is a critical application of computer vision that automatically identifies inappropriate visual content. This technology serves as the backbone for:</p><ul><li><strong>Content Moderation</strong>: Social media platforms, forums, and user-generated content sites</li><li><strong>Workplace Safety</strong>: Corporate environments and educational institutions</li><li><strong>Parental Controls</strong>: Family-friendly applications and services</li><li><strong>Compliance</strong>: Legal and regulatory requirements across industries</li><li><strong>Cloud Storage</strong>: Automatic flagging of inappropriate uploads</li></ul><p><a href="https://huggingface.co/Falconsai/nsfw_image_detection">Falconsai/nsfw_image_detection · Hugging Face</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vxk5YST3pFO9QcnO2Vj8XQ.png" /></figure><h3>Why Vision Transformers for Image Classification?</h3><p>Traditional computer vision relied heavily on Convolutional Neural Networks (CNNs), but Vision Transformers (ViTs) have revolutionized the field by adapting the transformer architecture — originally designed for natural language processing — to computer vision tasks.</p><h4>Key Advantages of Vision Transformers:</h4><ol><li><strong>Global Context Understanding</strong>: Unlike CNNs that process images through local convolutions, ViTs can capture long-range dependencies across the entire image from the first layer.</li><li><strong>Scalability</strong>: ViTs scale better with larger datasets and model sizes, often outperforming CNNs when sufficient training data is available.</li><li><strong>Transfer Learning</strong>: Pre-trained ViTs transfer exceptionally well to downstream tasks, making them perfect for specialized applications like NSFW detection.</li><li><strong>Attention Mechanisms</strong>: The self-attention mechanism allows the model to focus on the most relevant parts of an image for classification.</li></ol><h4>Getting Started with NSFW Detection using Vision Transformers</h4><p>Hugging Face makes it incredibly simple to get started with Vision Transformers for image classification. You can use the pipeline abstraction to perform NSFW detection with just a few lines of code:</p><pre>from transformers import pipeline<br><br>classifier = pipeline(&quot;image-classification&quot;, model=&quot;Falconsai/nsfw_image_detection&quot;)<br>result = classifier(&quot;/Users/tharindu/Downloads/1747220469461.jpeg&quot;)<br>print(f&quot;Image classification result: {result}&quot;)</pre><p>This one-liner abstracts away the complexities of preprocessing and inference, giving you a quick NSFW classification result from the Falconsai/nsfw_image_detection model hosted on Hugging Face Hub.</p><p>However, if you want finer control over the process (e.g., logging memory usage, optimizing preprocessing, or inspecting model outputs), you can manually load the model and run inference as follows:</p><pre>from transformers import AutoModelForImageClassification, ViTImageProcessor<br>from PIL import Image<br>import torch<br>import psutil<br><br># Load the model and processor<br>model = AutoModelForImageClassification.from_pretrained(&quot;Falconsai/nsfw_image_detection&quot;)<br>processor = ViTImageProcessor.from_pretrained(&quot;Falconsai/nsfw_image_detection&quot;)<br><br>print(f&quot;Model loaded. RAM used: {psutil.virtual_memory().used / (1024**2):.2f} MB&quot;)<br><br># Load your image (replace &#39;your_image.jpg&#39; with your actual image path)<br>image = Image.open(&quot;/Users/tharindu/Downloads/1747220469461.jpeg&quot;).convert(&quot;RGB&quot;)<br><br># Preprocess the image<br>inputs = processor(images=image, return_tensors=&quot;pt&quot;)<br><br># Perform inference<br>with torch.no_grad():<br>    outputs = model(**inputs)<br><br># Get predicted class<br>logits = outputs.logits<br>predicted_class_idx = logits.argmax(-1).item()<br>labels = model.config.id2label<br>print(f&quot;Predicted class: {labels[predicted_class_idx]}&quot;)</pre><h4>Explanation</h4><ul><li><strong>Pipeline Abstraction:</strong> The pipeline API is ideal for quick testing or prototyping. It encapsulates model loading, image preprocessing, and result interpretation in one line.</li><li><strong>Manual Inference:</strong> If you’re preparing for production deployment or need deeper insights (like memory profiling, detailed logits, or batched inference), using the lower-level AutoModelForImageClassification and ViTImageProcessor gives you that flexibility.</li><li><strong>RAM Monitoring:</strong> Incorporating psutil allows you to track resource usage, which is especially useful in constrained environments or when deploying on shared infrastructure.</li><li><strong>Image Preprocessing:</strong> The Vision Transformer model expects images to be correctly resized and normalized, which is handled seamlessly by ViTImageProcessor.</li></ul><p>This hands-on example bridges the gap between theory and practical deployment — giving you full control to scale your NSFW detection capabilities from local development to cloud-native production systems.</p><h3>Part II: From Development to Production — Building an NSFW Detection API</h3><p>In the previous section, we explored how to quickly load a Vision Transformer (ViT) model for NSFW detection using Hugging Face’s pipeline abstraction as well as the more advanced manual loading technique via AutoModelForImageClassification and ViTImageProcessor. Both approaches are great for rapid experimentation, especially within Jupyter notebooks. They allow us to test model predictions, debug data pipelines, and visualize results — all within a flexible and interactive environment.</p><p>However, notebooks are <strong>not</strong> where production begins.</p><h4>From Notebook to Real-World: The Production Challenge</h4><p>While notebooks are perfect for prototyping, deploying AI solutions in real-world systems demands more than just print() statements:</p><ul><li>✅ <strong>Performance</strong> (speed and efficiency)</li><li>✅ <strong>Scalability</strong> (handle concurrent requests)</li><li>✅ <strong>Monitoring</strong> (track uptime, latency, errors)</li><li>✅ <strong>Security</strong> (API authentication, rate limiting)</li><li>✅ <strong>Deployment Flexibility</strong> (Docker, Kubernetes, CI/CD)</li></ul><p>This section focuses on how we bridge that gap: taking our NSFW image classification model and preparing it for <strong>production-grade deployment</strong> using FastAPI, ONNX, and best practices in observability, security, and packaging.</p><h3>Understanding the Files Behind a Hugging Face Model</h3><p>Before jumping into model conversion and deployment, let’s inspect what’s actually included in the Hugging Face model repository.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fp-RZ3E6HtpSyfUwT7uUjQ.png" /></figure><p>As shown in the screenshot above from the <a href="https://huggingface.co/Falconsai/nsfw_image_detection">Falconsai/nsfw_image_detection</a> model page, we get several important files:</p><ul><li><strong>model.safetensors</strong> — The default and recommended format used by Hugging Face pipelines for secure and fast model loading.</li><li><strong>config.json </strong>— Contains the model configuration, including architecture details, input sizes, and label mappings.</li><li><strong>labels.json </strong>— Maps output indices to class labels like “nsfw” or “normal”.</li><li><strong>preprocessor_config.json</strong> — Used by the image processor for resizing and normalization.</li><li><strong>pytorch_model.bin</strong> — The original PyTorch model (legacy format).</li><li><strong>falconsai_yolov9_nsfw_model_quantized.pt</strong> — A quantized variant for YOLO-based use cases (not relevant for ViT).</li></ul><h3>Why Convert to ONNX?</h3><p>To transition from research-grade notebooks to <strong>high-performance production systems</strong>, we must consider optimizing our model’s runtime environment. That’s where ONNX comes in.</p><h4>🔄 ONNX (Open Neural Network Exchange)</h4><p>ONNX is a universal model format supported by many frameworks, designed specifically for <strong>interoperability and inference optimization</strong>.</p><h4>✅ Benefits of ONNX for Production:</h4><ul><li>🚀 <strong>Faster Inference</strong>: ONNX models run 2–5x faster than PyTorch on CPU thanks to static graphs and optimization.</li><li>🧠 <strong>Cross-Platform Compatibility</strong>: Works on Windows, Linux, macOS, mobile, and even embedded systems.</li><li>💡 <strong>Lightweight</strong>: The ONNX file is significantly smaller in memory footprint.</li><li>🛠️ <strong>Tooling Ecosystem</strong>: Supported by ONNX Runtime, TensorRT, OpenVINO, and more.</li><li>🔄 <strong>Dynamic Batching</strong>: Efficient for serving multiple requests with different input sizes.</li><li>📉 <strong>Lower Memory Consumption</strong>: Better for cost-efficient deployments in production environments.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d8eurxWJVPxcuwCXR3IYEg.png" /></figure><h4>Converting Our Hugging Face Model to ONNX</h4><p>Since Hugging Face does <strong>not</strong> provide a pre-converted ONNX model in this repo, we must convert it manually using torch.onnx.export().</p><p>Here’s a simplified conversion script used in our project:</p><pre>import torch<br>import os<br>from transformers import AutoModelForImageClassification<br><br>def convert_model():<br>    &quot;&quot;&quot;Convert Hugging Face model to ONNX format.&quot;&quot;&quot;<br>    print(&quot;🔄 Converting model to ONNX...&quot;)<br>    <br>    model_name = &quot;Falconsai/nsfw_image_detection&quot;<br>    <br>    # Load model<br>    model = AutoModelForImageClassification.from_pretrained(model_name)<br>    model.eval()<br>    <br>    # Create dummy input (Vision Transformer expects 224x224 images)<br>    dummy_input = torch.randn(1, 3, 224, 224)<br>    <br>    # Export to ONNX<br>    onnx_path = &quot;../models/nsfw_model.onnx&quot;<br>    torch.onnx.export(<br>        model,<br>        dummy_input,<br>        onnx_path,<br>        export_params=True,<br>        opset_version=14,<br>        do_constant_folding=True,<br>        input_names=[&#39;input&#39;],<br>        output_names=[&#39;output&#39;],<br>        dynamic_axes={<br>            &#39;input&#39;: {0: &#39;batch_size&#39;},<br>            &#39;output&#39;: {0: &#39;batch_size&#39;}<br>        }<br>    )</pre><p>The key aspects of this conversion:</p><ul><li><strong>Model evaluation mode</strong>: model.eval() disables training-specific layers</li><li><strong>Dynamic batch sizes</strong>: Allows flexible input dimensions</li><li><strong>Constant folding</strong>: Pre-computes constant operations for optimization</li><li><strong>Opset version 14</strong>: Ensures compatibility with ONNX Runtime</li></ul><h3>API Architecture: Building for Production</h3><p>Our NSFW detection API follows production best practices with a modular architecture:</p><pre>nsfw-api/<br>├── api/                    # Main package<br>│   ├── core/               # Core functionality<br>│   │   ├── auth.py         # Authentication &amp; rate limiting<br>│   │   ├── config.py       # Configuration management<br>│   │   ├── metrics.py      # Prometheus metrics<br>│   │   └── utils.py        # Utility functions<br>│   ├── models/             # Data models<br>│   │   └── models.py       # Pydantic response models<br>│   └── services/           # Business logic<br>│       └── inference.py    # ML model inference<br>├── main.py                 # FastAPI application<br>├── models/                 # ONNX model files<br>└── scripts/                # Utility scripts</pre><h3>Configuration Management</h3><p>Production applications require flexible configuration. Our config.py uses environment variables with sensible defaults:</p><pre>class Config:<br>    &quot;&quot;&quot;Production configuration settings.&quot;&quot;&quot;<br>    <br>    # Model settings<br>    MODEL_PATH = os.getenv(&quot;MODEL_PATH&quot;, &quot;models/nsfw_model.onnx&quot;)<br>    MAX_IMAGE_SIZE = int(os.getenv(&quot;MAX_IMAGE_SIZE&quot;, &quot;10485760&quot;))  # 10MB<br>    SUPPORTED_FORMATS = {&quot;image/jpeg&quot;, &quot;image/jpg&quot;, &quot;image/png&quot;, &quot;image/webp&quot;, &quot;image/bmp&quot;}<br>    <br>    # API settings<br>    API_TITLE = &quot;NSFW Image Detection API&quot;<br>    API_VERSION = &quot;1.0.0&quot;<br>    API_DESCRIPTION = &quot;Production-grade NSFW content detection API&quot;<br>    <br>    # Performance settings<br>    MAX_WORKERS = int(os.getenv(&quot;MAX_WORKERS&quot;, &quot;4&quot;))<br>    REQUEST_TIMEOUT = int(os.getenv(&quot;REQUEST_TIMEOUT&quot;, &quot;30&quot;))<br>    <br>    # Security settings<br>    API_KEY = os.getenv(&quot;API_KEY&quot;, &quot;your-secure-api-key-here&quot;)<br>    ALLOWED_HOSTS = os.getenv(&quot;ALLOWED_HOSTS&quot;, &quot;*&quot;).split(&quot;,&quot;)<br>    <br>    # Rate limiting (requests per minute)<br>    RATE_LIMIT = int(os.getenv(&quot;RATE_LIMIT&quot;, &quot;100&quot;))</pre><h3>Authentication and Security</h3><p>Security is paramount in production APIs. Our implementation includes:</p><h4>API Key Authentication</h4><pre>class APIKeyAuth:<br>    def __init__(self, api_key: str):<br>        self.api_key = api_key<br>    <br>    def __call__(self, authorization: str = Header(None)):<br>        if not authorization:<br>            raise HTTPException(<br>                status_code=401, <br>                detail=&quot;Authorization header required&quot;<br>            )<br>        <br>        if not authorization.startswith(&quot;Bearer &quot;):<br>            raise HTTPException(<br>                status_code=401, <br>                detail=&quot;Invalid authorization format&quot;<br>            )<br>        <br>        token = authorization.split(&quot; &quot;)[1]<br>        if token != self.api_key:<br>            raise HTTPException(<br>                status_code=401, <br>                detail=&quot;Invalid API key&quot;<br>            )<br>        <br>        return token</pre><h4>Rate Limiting</h4><pre>class RateLimiter:<br>    def __init__(self, requests_per_minute: int):<br>        self.requests_per_minute = requests_per_minute<br>        self.requests = defaultdict(list)<br>    <br>    def is_allowed(self, client_ip: str) -&gt; bool:<br>        now = time.time()<br>        minute_ago = now - 60<br>        <br>        # Clean old requests<br>        self.requests[client_ip] = [<br>            req_time for req_time in self.requests[client_ip] <br>            if req_time &gt; minute_ago<br>        ]<br>        <br>        # Check if under limit<br>        if len(self.requests[client_ip]) &lt; self.requests_per_minute:<br>            self.requests[client_ip].append(now)<br>            return True<br>        <br>        return False</pre><h3>Model Inference with Thread Safety</h3><p>Production systems need to handle concurrent requests safely. Our ModelManager class provides thread-safe inference:</p><pre>class ModelManager:<br>    def __init__(self):<br>        self.session = None<br>        self.labels = {0: &quot;normal&quot;, 1: &quot;nsfw&quot;}<br>        self.model_loaded = False<br>        self.load_time = None<br>        self.executor = ThreadPoolExecutor(max_workers=Config.MAX_WORKERS)<br>    <br>    def load_model(self):<br>        &quot;&quot;&quot;Load ONNX model with error handling.&quot;&quot;&quot;<br>        start_time = time.time()<br>        <br>        try:<br>            if not os.path.exists(Config.MODEL_PATH):<br>                raise FileNotFoundError(f&quot;Model not found: {Config.MODEL_PATH}&quot;)<br>            <br>            self.session = onnxruntime.InferenceSession(<br>                Config.MODEL_PATH,<br>                providers=[&#39;CPUExecutionProvider&#39;]<br>            )<br>            <br>            self.model_loaded = True<br>            self.load_time = time.time() - start_time<br>            <br>        except Exception as e:<br>            logger.error(f&quot;Failed to load model: {e}&quot;)<br>            self.model_loaded = False<br>            raise<br>    <br>    async def predict_async(self, image: Image.Image) -&gt; Dict[str, Any]:<br>        &quot;&quot;&quot;Thread-safe async prediction.&quot;&quot;&quot;<br>        loop = asyncio.get_event_loop()<br>        return await loop.run_in_executor(self.executor, self._predict_sync, image)</pre><h3>Monitoring and Observability</h3><p>Production systems require comprehensive monitoring. We implement Prometheus metrics:</p><h4>HTTP Metrics</h4><pre># Request counting<br>HTTP_REQUESTS_TOTAL = Counter(<br>    &#39;http_requests_total&#39;,<br>    &#39;Total HTTP requests&#39;,<br>    [&#39;method&#39;, &#39;endpoint&#39;, &#39;status_code&#39;]<br>)<br><br># Request latency<br>HTTP_REQUEST_DURATION = Histogram(<br>    &#39;http_request_duration_seconds&#39;,<br>    &#39;HTTP request duration in seconds&#39;,<br>    [&#39;method&#39;, &#39;endpoint&#39;]<br>)<br># Active requests<br>HTTP_REQUESTS_ACTIVE = Gauge(<br>    &#39;http_requests_active&#39;,<br>    &#39;Number of active HTTP requests&#39;<br>)</pre><h4>ML-Specific Metrics</h4><pre># Prediction tracking<br>NSFW_PREDICTIONS_TOTAL = Counter(<br>    &#39;nsfw_predictions_total&#39;,<br>    &#39;Total NSFW predictions made&#39;,<br>    [&#39;prediction&#39;]<br>)<br><br># Inference performance<br>NSFW_INFERENCE_DURATION = Histogram(<br>    &#39;nsfw_inference_duration_seconds&#39;,<br>    &#39;Time spent on inference&#39;<br>)<br># Model health<br>NSFW_MODEL_LOADED = Gauge(<br>    &#39;nsfw_model_loaded&#39;,<br>    &#39;Whether the NSFW model is loaded&#39;<br>)</pre><h3>API Endpoints</h3><p>Our API provides multiple endpoints for different use cases:</p><h4>1. Image Upload Prediction</h4><pre>@app.post(&quot;/predict&quot;, response_model=PredictionResponse)<br>async def predict_image(request: Request, file: UploadFile = File(...)):<br>    &quot;&quot;&quot;<br>    Predict NSFW content in uploaded image.<br>    <br>    Features:<br>    - File validation and size limits<br>    - Rate limiting by client IP<br>    - Comprehensive error handling<br>    - Performance tracking<br>    &quot;&quot;&quot;</pre><h4>2. URL-Based Prediction</h4><pre>@app.post(&quot;/predict/url&quot;)<br>async def predict_image_url(/service/request: Request, image_url: str):<br>    &quot;&quot;&quot;<br>    Predict NSFW content from image URL.<br>    <br>    Features:<br>    - URL validation and timeout handling<br>    - Content-type verification<br>    - Safe image downloading with limits<br>    &quot;&quot;&quot;</pre><h4>3. Health Check</h4><pre>@app.get(&quot;/health&quot;, response_model=HealthResponse)<br>async def health_check():<br>    &quot;&quot;&quot;<br>    System health endpoint for load balancers.<br>    <br>    Returns:<br>    - Service status<br>    - Model loading state<br>    - Uptime information<br>    - API version<br>    &quot;&quot;&quot;</pre><h4>4. Metrics Export</h4><pre>@app.get(&quot;/metrics&quot;)<br>async def get_metrics():<br>    &quot;&quot;&quot;Prometheus metrics endpoint for monitoring.&quot;&quot;&quot;<br>    prometheus_metrics = metrics.get_prometheus_metrics()<br>    return Response(content=prometheus_metrics, media_type=metrics.CONTENT_TYPE_LATEST)</pre><h3>Error Handling and Logging</h3><p>Robust error handling is crucial for production systems:</p><pre>@app.exception_handler(HTTPException)<br>async def http_exception_handler(request: Request, exc: HTTPException):<br>    &quot;&quot;&quot;Custom HTTP exception handler with structured logging.&quot;&quot;&quot;<br>    logger.error(f&quot;HTTP {exc.status_code}: {exc.detail} - Path: {request.url.path}&quot;)<br>    <br>    return JSONResponse(<br>        status_code=exc.status_code,<br>        content={<br>            &quot;success&quot;: False,<br>            &quot;error&quot;: exc.detail,<br>            &quot;status_code&quot;: exc.status_code,<br>            &quot;path&quot;: request.url.path,<br>            &quot;timestamp&quot;: time.time()<br>        }<br>    )</pre><h3>Input Validation with Pydantic</h3><p>We use Pydantic models for request/response validation:</p><pre>class PredictionResponse(BaseModel):<br>    success: bool<br>    request_id: str<br>    prediction: Dict[str, Any]<br>    processing_time_ms: float<br><br>class HealthResponse(BaseModel):<br>    status: str<br>    model_loaded: bool<br>    version: str<br>    uptime_seconds: float<br>class ErrorResponse(BaseModel):<br>    error: str<br>    error_code: str<br>    request_id: str</pre><h3>🧪 Local API Testing with Uvicorn and Postman</h3><p>Once the API is architected, validated, and all production-grade features (like authentication, rate limiting, and monitoring) are in place, the next natural step is to run it <strong>locally on your machine</strong> to verify that everything works as expected.</p><p>Instead of immediately jumping to containerization or deployment, this intermediate step helps ensure:</p><ul><li>The ONNX model loads correctly</li><li>All API endpoints are reachable</li><li>Authentication and rate limiting work as configured</li><li>You can make real predictions using tools like <strong>Postman</strong> or <strong>curl</strong></li></ul><h3>▶️ Starting the API with</h3><h3>uvicorn</h3><p>We use uvicorn as our ASGI server to run the FastAPI app. The following log output confirms a successful local launch and ONNX model loading:</p><pre>git clone https://github.com/tharindu-sankalpa/huggingface-learning.git<br><br>cd huggingface-learning<br>uv sync<br><br>cd nsfw-api<br><br>export API_KEY=&quot;your-secure-production-key&quot;<br>export MODEL_PATH=&quot;models/nsfw_model.onnx&quot;<br><br>uvicorn main:app --reload --host 0.0.0.0 --port 8000</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*R7qaxgFer9M-nqmnabN-Ow.png" /></figure><p>As shown above, the model was loaded in just over <strong>3 seconds</strong>, and inference providers (CoreML and CPU) are successfully detected.</p><h3>📬 Making Requests Using Postman</h3><p>To interact with the API, we use <strong>Postman</strong> to test the /predict endpoint by uploading an image.</p><p>Here’s a snapshot from Postman showing a successful image classification call:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LnJJv3eYrMG1wkQNRWOA9g.png" /></figure><p>In this example:</p><ul><li><strong>Authorization</strong>: A Bearer token (API_KEY) is added to the header</li><li><strong>Endpoint</strong>: POST /predict</li><li><strong>Body</strong>: Image file uploaded via form-data</li><li><strong>Response</strong>: JSON payload with predicted label, confidence scores, and request ID</li></ul><pre>{<br>  &quot;success&quot;: true,<br>  &quot;request_id&quot;: &quot;8732b86c-bt68-4c79-9cf3-c6e97898dd43&quot;,<br>  &quot;prediction&quot;: {<br>    &quot;label&quot;: &quot;nsfw&quot;,<br>    &quot;confidence&quot;: 0.9698140025138855,<br>    &quot;probabilities&quot;: {<br>      &quot;normal&quot;: 0.03018589444759457,<br>      &quot;nsfw&quot;: 0.969814025188855<br>    },<br>    &quot;is_safe&quot;: false<br>  },<br>  &quot;processing_time_ms&quot;: 462.95,<br>  &quot;model_version&quot;: &quot;1.0&quot;<br>}</pre><p>This confirms that:</p><ul><li>The API is correctly classifying the image</li><li>The label nsfw has a high confidence score</li><li>The response time is ~463ms — a solid benchmark for CPU-based inference</li></ul><h3>Deployment and Production Considerations</h3><h4>🚀 Next Step: Containerize and Deploy</h4><p>Now that the API behaves correctly on your local machine, the next logical step is to <strong>containerize the service</strong> and prepare it for cloud deployment. Containerization provides:</p><ul><li><strong>Environment consistency</strong> across dev/staging/prod</li><li><strong>Portability</strong> to any infrastructure</li><li><strong>Scalability</strong> through orchestrators like Kubernetes</li></ul><p>We’ll begin by building a lightweight Docker image and testing it locally. From there, we’ll move toward deploying the API in scalable, cloud-native environments.</p><blockquote><em>🔜 </em><strong><em>Coming Up</em></strong><em>: In the </em><strong><em>next article</em></strong><em>, we’ll dive deep into </em><strong><em>production deployment using Kubernetes</em></strong><em>, where we’ll cover:</em></blockquote><ul><li>Creating Dockerfiles and optimizing image size</li><li>Deploying to Kubernetes with Helm</li><li>Scaling with Horizontal Pod Autoscalers</li><li>Securing endpoints with Ingress and TLS</li><li>Logging and monitoring with Prometheus + Grafana</li><li>Rolling updates and zero-downtime deployment</li></ul><p>This upcoming content will guide you from a single-node test setup to a fully managed, production-grade Kubernetes deployment suitable for high availability and enterprise use.</p><h4>Docker Deployment</h4><pre>FROM python:3.11-slim<br><br>WORKDIR /app<br>COPY . .<br>RUN pip install uv<br>RUN uv sync --frozen<br>EXPOSE 8000<br>CMD [&quot;python&quot;, &quot;main.py&quot;]</pre><h4>Environment Configuration</h4><pre># Production environment variables<br><br>export RATE_LIMIT=&quot;1000&quot;<br>export MAX_WORKERS=&quot;8&quot;<br>export ALLOWED_HOSTS=&quot;api.yourcompany.com,*.yourcompany.com&quot;</pre><h4>Performance Optimizations</h4><ol><li><strong>ONNX Runtime</strong>: 2–5x faster inference</li><li><strong>Async processing</strong>: Handle concurrent requests</li><li><strong>Thread pool</strong>: Dedicated workers for CPU-intensive tasks</li><li><strong>Caching</strong>: Model loaded once, used many times</li><li><strong>Memory management</strong>: Efficient image processing</li></ol><h3>Performance Metrics and Benchmarks</h3><p>Our production API delivers impressive performance metrics:</p><h3>Speed Benchmarks</h3><ul><li><strong>Inference Time</strong>: ~300–400ms per image on CPU</li><li><strong>Memory Usage</strong>: ~2GB RAM with model loaded</li><li><strong>Throughput</strong>: 100+ requests/minute with 4 workers</li><li><strong>Model Size</strong>: 150MB (ONNX) vs 330MB (PyTorch)</li></ul><h3>Accuracy Metrics</h3><ul><li><strong>NSFW Detection</strong>: High precision for content classification</li><li><strong>False Positive Rate</strong>: &lt; 1% on test datasets</li><li><strong>Confidence Scores</strong>: Calibrated probability outputs</li></ul><h3>System Metrics</h3><ul><li><strong>Startup Time</strong>: ~5 seconds for model loading</li><li><strong>Memory Efficiency</strong>: 50% reduction vs PyTorch</li><li><strong>CPU Utilization</strong>: Optimized for multi-core systems</li></ul><h3>Production Features Deep Dive</h3><h3>Authentication System</h3><p>Our API implements Bearer token authentication:</p><ul><li><strong>Security</strong>: Prevents unauthorized access</li><li><strong>Flexibility</strong>: Easy to integrate with existing auth systems</li><li><strong>Monitoring</strong>: Track usage by API key</li></ul><h3>Rate Limiting</h3><p>Protects against abuse and ensures fair usage:</p><ul><li><strong>Per-IP limiting</strong>: 100 requests/minute default</li><li><strong>Sliding window</strong>: Accurate rate calculation</li><li><strong>Graceful degradation</strong>: Clear error messages</li></ul><h3>Monitoring and Alerting</h3><p>Comprehensive observability with Prometheus metrics:</p><ul><li><strong>Request metrics</strong>: Count, latency, error rates</li><li><strong>ML metrics</strong>: Prediction distribution, confidence scores</li><li><strong>System metrics</strong>: Memory usage, model health</li></ul><h3>Error Handling</h3><p>Production-ready error management:</p><ul><li><strong>Structured logging</strong>: JSON-formatted logs</li><li><strong>Request tracing</strong>: Unique request IDs</li><li><strong>Graceful failures</strong>: Clear error messages</li></ul><h3>Use Cases and Applications</h3><h3>Content Moderation</h3><ul><li><strong>Social media platforms</strong>: Automatic content filtering</li><li><strong>Cloud storage</strong>: Safe file sharing</li><li><strong>E-commerce</strong>: Product image validation</li></ul><h3>Compliance and Safety</h3><ul><li><strong>Corporate environments</strong>: Policy enforcement</li><li><strong>Educational platforms</strong>: Safe browsing</li><li><strong>Family-friendly services</strong>: Content protection</li></ul><h3>Custom Integration</h3><ul><li><strong>Webhook support</strong>: Real-time notifications</li><li><strong>Batch processing</strong>: High-volume image analysis</li><li><strong>White-label solutions</strong>: Custom branding</li></ul><h3>Conclusion</h3><p>This journey from Hugging Face fundamentals to production deployment demonstrates the evolution of modern AI development. We’ve covered:</p><ol><li><strong>Hugging Face Ecosystem</strong>: Understanding the tools and platforms</li><li><strong>Model Optimization</strong>: Converting PyTorch to ONNX for production</li><li><strong>API Design</strong>: Building scalable, secure, and monitored services</li><li><strong>Production Practices</strong>: Authentication, rate limiting, and observability</li></ol><p>The combination of Hugging Face’s accessibility with production engineering principles creates powerful AI applications that can scale to serve millions of users.</p><h3>Key Takeaways</h3><ol><li><strong>Start Simple</strong>: Use Hugging Face pipelines for rapid prototyping</li><li><strong>Optimize for Production</strong>: Convert models to ONNX for better performance</li><li><strong>Design for Scale</strong>: Implement proper authentication, monitoring, and error handling</li><li><strong>Monitor Everything</strong>: Use metrics to understand system behavior</li><li><strong>Iterate and Improve</strong>: Production is an ongoing process, not a destination</li></ol><h3>Next Steps</h3><p>To extend this project, consider:</p><ul><li><strong>GPU acceleration</strong> with CUDA/TensorRT</li><li><strong>Batch processing</strong> for improved throughput</li><li><strong>Model ensembling</strong> for higher accuracy</li><li><strong>A/B testing</strong> for model improvements</li><li><strong>Auto-scaling</strong> with Kubernetes</li></ul><p>The future of AI lies not just in better models, but in better production systems that make AI accessible, reliable, and impactful. This project serves as a blueprint for that future.</p><p><em>Built with ❤️ using Hugging Face Transformers, FastAPI, ONNX Runtime, and modern Python best practices. The complete source code is available for educational and demonstration purposes.</em></p><p><strong>Repository Structure:</strong></p><ul><li>notebook.ipynb - Interactive learning examples</li><li>nsfw-api/ - Production API implementation</li><li>test_installation.py - Setup verification</li><li>README.md - Project documentation</li></ul><p><strong>Get Started:</strong></p><pre>git clone &lt;repository-url&gt;<br>cd huggingface-learning<br>uv sync<br>export API_KEY=&quot;your-api-key&quot;<br>cd nsfw-api &amp;&amp; python main.py</pre><p><strong>API Documentation:</strong> Visit http://localhost:8000/docs after starting the API for interactive documentation.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fa04de9e4a92" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Enterprise MLOps Platform on Kubernetes: Experiment Tracking Fundamentals with MLflow]]></title>
            <link>https://medium.com/@wltsankalpa/enterprise-mlops-platform-on-kubernetes-experiment-tracking-fundamentals-with-mlflow-303dc95d5d63?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/303dc95d5d63</guid>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Sun, 13 Jul 2025 16:13:31 GMT</pubDate>
            <atom:updated>2025-07-13T16:13:31.055Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Part 3 of the Enterprise MLOps Series</em></p><h3>Introduction: From Infrastructure to Insights</h3><p>In <a href="https://medium.com/@wltsankalpa/enterprise-mlops-platform-on-kubernetes-complete-machine-learning-life-cycle-from-experimentation-b78a1c21e7c5">Part 1 of this series</a>, we explored the architectural foundation of an enterprise MLOps platform, identifying the critical challenges that plague ML production deployments: the experiment explosion problem, the “golden model” mystery, and production integration challenges.</p><p><a href="https://medium.com/@wltsankalpa/enterprise-mlops-platform-on-kubernetes-complete-machine-learning-life-cycle-from-experimentation-b78a1c21e7c5">Enterprise MLOps Platform on Kubernetes: Complete Machine Learning Life Cycle from Experimentation…</a></p><p>In <a href="https://medium.com/@wltsankalpa/enterprise-mlops-platform-on-kubernetes-setting-up-a-production-grade-mlflow-tracking-server-0ea0f49d8363">Part 2</a>, we moved from architecture to implementation by deploying a production-grade MLflow tracking server on Azure Kubernetes Service (AKS). We established a robust, scalable foundation with PostgreSQL for metadata storage, Azure Blob Storage for artifacts, and proper authentication mechanisms.</p><p><a href="https://medium.com/@wltsankalpa/enterprise-mlops-platform-on-kubernetes-setting-up-a-production-grade-mlflow-tracking-server-0ea0f49d8363">Enterprise MLOps Platform on Kubernetes: Setting Up a Production-Grade MLflow Tracking Server</a></p><p>Now, with our MLflow infrastructure operational, it’s time to tackle the next critical challenge: <strong>actually using it for comprehensive experiment tracking</strong>. This article will transform your data science workflow from scattered notebooks and lost experiments to a systematic, reproducible process that captures every aspect of your ML experimentation.</p><h4>What We’ll Accomplish</h4><p>By the end of this article, you’ll understand how to:</p><ul><li>Connect your development environment to the centralized MLflow server</li><li>Implement systematic experiment tracking for data ingestion and exploration</li><li>Organize experiments with proper artifact management and metadata logging</li><li>Resolve common authentication and permission challenges</li><li>Set up preprocessing pipelines ready for advanced training workflows</li></ul><p>We’ll use the UCI Adult Income dataset as our practical example, demonstrating how proper experiment tracking transforms typical data science workflows from chaos to clarity.</p><h4>Understanding Our Centralized MLflow Architecture</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2iFKf9Ea5K0TT60RdVGyrA.png" /></figure><p>Before diving into experiment tracking, let’s quickly review the architecture we deployed in Part 2. This understanding is crucial for effectively using the system.</p><p><strong>Key Components:</strong></p><ul><li><strong>MLflow Server</strong>: Runs in your AKS cluster, handling API requests and serving the web UI</li><li><strong>PostgreSQL Database</strong>: Stores experiment metadata (parameters, metrics, run information)</li><li><strong>Azure Blob Storage</strong>: Stores artifacts (models, plots, datasets, reports)</li><li><strong>LoadBalancer Service</strong>: Provides external access to the MLflow UI and API</li></ul><p>This setup provides a robust, scalable, and collaborative platform for managing the machine learning lifecycle, with metadata in PostgreSQL and artifacts in Azure Blob Storage ensuring persistence and reliability.</p><h3>Connecting to Your Centralized MLflow Server from Python</h3><p>Now that our MLflow server is up, running, and we understand its architecture, the next crucial step is to connect to it from our Python-based machine learning workflows (e.g., Jupyter notebooks, Python scripts). This will allow us to send all our experiment parameters, metrics, and artifacts to the centralized server for tracking.</p><p>We’ll use environment variables to store the MLflow server’s tracking URI and credentials. This is a good practice as it keeps sensitive information out of your codebase and makes it easy to configure different environments. We’ll use a .env file to manage these variables locally.</p><p><strong>Step 1: Create a .env File</strong></p><p>In the root directory of your Python project or where your Jupyter notebook resides, create a file named .env. Add the following content to this file, replacing the placeholder values with your actual MLflow server details:</p><pre># .env file<br><br># MLflow UI Admin Username (configured in mlflow-values.yaml)<br>MLFLOW_TRACKING_USERNAME=admin<br><br># MLFLOW UI Admin Password (configured in mlflow-values.yaml - must be &gt;12 chars)<br># Replace this with the actual password you set in mlflow-values.yaml<br>MLFLOW_TRACKING_PASSWORD=&quot;&lt;YOUR_MLFLOW_UI_ADMIN_PASSWORD&gt;&quot;<br><br># MLflow Tracking Server URI<br># Replace &lt;YOUR_EXTERNAL_IP&gt; with the External IP of your MLflow LoadBalancer service<br>MLFLOW_TRACKING_URI=&quot;http://&lt;YOUR_EXTERNAL_IP&gt;&quot;</pre><p><strong>Explanation of the .env variables:</strong></p><ul><li>MLFLOW_TRACKING_USERNAME: The username you configured for MLflow UI authentication (e.g., admin).</li><li>MLFLOW_TRACKING_PASSWORD: The password corresponding to the adminUsername that you set in mlflow-values.yaml. <strong>Make sure this matches exactly what you set and is enclosed in quotes if it contains special characters.</strong></li><li>MLFLOW_TRACKING_URI: This is the URL of your MLflow server. It should be http://&lt;YOUR_EXTERNAL_IP&gt;, where &lt;YOUR_EXTERNAL_IP&gt; is the public IP address of the mlflow LoadBalancer service you obtained from kubectl get svc -n $K8S_NAMESPACE. Since your service is on port 80, you don’t need to specify the port in the URI.</li></ul><p><strong>Important Security Note:</strong> Add .env to your project’s .gitignore file to prevent accidentally committing sensitive credentials to version control.</p><p><strong>Step 2: Add Dependencies with Poetry</strong></p><p>It is recommended that you use <a href="https://www.google.com/url?sa=E&amp;q=https%3A%2F%2Fpython-poetry.org%2F">Poetry</a> for dependency management in your project. You’ll add python-dotenv (and mlflow if it’s not already a project dependency) using the Poetry CLI. My previous article covered more details about Poetry usage.</p><p><a href="https://medium.com/@wltsankalpa/demystifying-the-langchain-ecosystem-for-llm-powered-application-development-6c1fbff064ea">Demystifying the LangChain Ecosystem for LLM-Powered Application Development</a></p><p>Open your terminal in the root directory of your Poetry project (where your pyproject.toml file is located) and run:</p><pre>poetry add python-dotenv mlflow scikit-learn pandas matplotlib seaborn <br>poetry add azure-identity azure-storage-blob tensorflow xgboost<br>poetry add --group dev notebook ipykernel</pre><p><strong>Step 3: Connect to MLflow in Your Python Code</strong></p><p>Now, in your Jupyter notebook or Python script, you can load these environment variables and tell MLflow where to send its data.</p><p><strong>How to use mlflow.start_run() and mlflow.end_run() in a Notebook:</strong></p><p>In a notebook environment, you often want a single MLflow run to span across multiple cells. For example, one cell might load data, another might preprocess it, and a third might train a model — all part of the same experimental “run.”</p><p>Here’s how you can set it up:</p><pre># --- Cell 1: Environment Setup and Start MLflow Run ---<br>from dotenv import load_dotenv<br>import os<br>import mlflow<br><br># Load environment variables from .env file<br>load_dotenv()<br><br># Get credentials and URI from environment variables<br>MLFLOW_USERNAME = os.getenv(&#39;MLFLOW_TRACKING_USERNAME&#39;)<br>MLFLOW_PASSWORD = os.getenv(&#39;MLFLOW_TRACKING_PASSWORD&#39;)<br>MLFLOW_TRACKING_URI = os.getenv(&#39;MLFLOW_TRACKING_URI&#39;)<br><br># These lines are crucial for MLflow to authenticate with your server<br>os.environ[&#39;MLFLOW_TRACKING_USERNAME&#39;] = MLFLOW_USERNAME<br>os.environ[&#39;MLFLOW_TRACKING_PASSWORD&#39;] = MLFLOW_PASSWORD<br><br># Set the tracking URI<br>mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)<br><br># Define an experiment name<br># If the experiment doesn&#39;t exist, MLflow creates it.<br>EXPERIMENT_NAME = &quot;UCI Adult Income Prediction - Centralized&quot;<br>mlflow.set_experiment(EXPERIMENT_NAME)<br><br># Start a new run manually. We&#39;ll end it later.<br># It&#39;s good practice to give your run a descriptive name.<br>current_run = mlflow.start_run(run_name=&quot;Initial_Setup_and_Connection_Test&quot;)<br>print(f&quot;MLflow Run Name: {current_run.data.tags.get(&#39;mlflow.runName&#39;)}&quot;)<br>print(f&quot;MLflow Run ID: {current_run.info.run_id}&quot;)<br>print(f&quot;MLflow Experiment ID: {current_run.info.experiment_id}&quot;)<br>print(f&quot;MLflow tracking URI: {mlflow.get_tracking_uri()}&quot;)<br>print(f&quot;MLflow artifact URI: {mlflow.get_artifact_uri()}&quot;)<br><br># You can log a simple parameter to test<br>mlflow.log_param(&quot;connection_status&quot;, &quot;successful&quot;)</pre><p>After running this cell, if everything is set up right (your URI, username, password are correct, and the server can access Azure Blob Storage), it should run without errors.</p><p>Go check your MLflow UI in the browser (at http://&lt;YOUR_EXTERNAL_IP&gt;).</p><ul><li>You should see a new experiment named “UCI Adult Income Prediction — Centralized” (or whatever you chose).</li><li>Inside this experiment, you should see a run named “Initial_Setup_and_Connection_Test” with a parameter “connection_status” logged.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D3FPmgm-SoSlMD4VHQIrJA.png" /><figcaption><em>Our first test run successfully logged to the centralized MLflow server!</em></figcaption></figure><p><strong>Logging more data…</strong><br>You can then have subsequent cells that log more parameters, metrics, or artifacts to this <em>same</em> run because it’s still active.</p><p><strong>Final Cell: End Run</strong><br>And then, in a final cell (or when you’re done with that logical block of work for the run), you explicitly end it:</p><pre># --- Cell X: End the MLflow Run ---<br>if mlflow.active_run(): # Check if a run is active before trying to end it<br>    mlflow.end_run()<br>    print(&quot;MLflow run ended.&quot;)<br>else:<br>    print(&quot;No active MLflow run to end.&quot;)</pre><p>This approach gives you flexibility in notebooks. The traditional with mlflow.start_run() as run: block is great for self-contained scripts because it automatically ends the run, even if errors occur. But for interactive notebook development, manual start_run() and end_run() can be more convenient.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5kHCCh8Q9LeUyuQCG8tS_w.png" /><figcaption>MLflow run ended</figcaption></figure><h4>Practical Application: The UCI Adult Income Dataset</h4><p>With our MLflow environment connected to our central server, let’s get practical. We’ll use the well-known <strong>UCI Adult Income dataset</strong>. The goal is to predict whether an individual’s income is more than $50,000 per year based on census data. This is a binary classification problem (two possible outcomes).</p><p>Throughout this project, we’ll see how to use MLflow to keep track of our data cleaning, exploratory data analysis (EDA), model training, and model checking steps.</p><h3>Phase 1: Data Ingestion and Initial Preparation</h3><p>First, we need to get the dataset. The ucimlrepo library is a handy way to download datasets directly from the UCI Machine Learning Repository.</p><p><strong>[IMPORTANT NOTE FOR YOU]:</strong> The following Python code block is designed to be run in a Jupyter notebook cell. It assumes you’ve already run the setup cell (Cell 1 from “Connect to MLflow in Your Python Code”) that sets the MLflow tracking URI and starts an MLflow run. We will be logging to the <em>active run</em> started previously, or if you prefer, you can start a new, dedicated run for this phase.</p><p>Let’s create a new run specifically for data ingestion.</p><pre># --- Notebook Cell: Data Ingestion and Initial Preparation ---<br># Make sure you&#39;ve run the initial MLflow setup cell first to set the tracking URI and experiment.<br><br>import pandas as pd<br>import json # For handling JSON data<br>from ucimlrepo import fetch_ucirepo # To get the dataset<br><br># --- Start a new MLflow run for this data loading &amp; prep phase ---<br># (This assumes your MLFLOW_TRACKING_URI, USERNAME, PASSWORD are set via os.environ<br># and mlflow.set_experiment() has been called from the previous setup cell)<br><br># If a run is already active from a previous cell, you might want to end it first<br>if mlflow.active_run():<br>    mlflow.end_run()<br>    print(&quot;Ended previous active run.&quot;)<br><br># Start a new run for data ingestion<br>data_ingestion_run = mlflow.start_run(run_name=&quot;Data_Ingestion_and_Initial_Prep&quot;)<br>print(f&quot;Starting new MLflow Run for Data Ingestion: {data_ingestion_run.data.tags.get(&#39;mlflow.runName&#39;)}&quot;)<br>print(f&quot;Run ID: {data_ingestion_run.info.run_id}&quot;)<br><br># --- 1. Fetch Dataset ---<br>print(&quot;Fetching UCI Adult dataset...&quot;)<br>try:<br>    adult_dataset_info = fetch_ucirepo(id=2) # id=2 is for the Adult dataset<br>    X_raw = adult_dataset_info.data.features<br>    y_raw = adult_dataset_info.data.targets<br>    print(&quot;Dataset fetched successfully.&quot;)<br>except Exception as e:<br>    print(f&quot;Error fetching dataset: {e}&quot;)<br>    mlflow.log_param(&quot;data_fetching_status&quot;, &quot;failed&quot;)<br>    mlflow.log_param(&quot;data_fetching_error&quot;, str(e))<br>    if mlflow.active_run(): mlflow.end_run() # End run if fetching fails<br>    raise # Re-raise the exception to stop execution<br><br># --- 2. Combine Features and Target ---<br># For easier handling, let&#39;s put features and target into one DataFrame<br>df_raw = pd.concat([X_raw, y_raw], axis=1)<br>print(&quot;\nCombined DataFrame head (first 5 rows):&quot;)<br>print(df_raw.head())<br><br># --- 3. Basic Data Type Conversion ---<br># Some integer columns might be better as floats for later processing<br># or to handle potential NaNs more consistently.<br>int_columns = df_raw.select_dtypes(include=&#39;int64&#39;).columns<br>if not int_columns.empty:<br>    df_raw[int_columns] = df_raw[int_columns].astype(&#39;float64&#39;)<br>    print(f&quot;\nConverted integer columns to float: {list(int_columns)}&quot;)<br>    mlflow.log_param(&quot;int_columns_converted_to_float&quot;, list(int_columns))<br>else:<br>    print(&quot;\nNo int64 columns found to convert.&quot;)<br>    mlflow.log_param(&quot;int_columns_converted_to_float&quot;, &quot;None&quot;)<br><br>target_column_name = y_raw.columns[0] # Get the target column name (e.g., &#39;income&#39;)<br><br># --- 4. Logging Dataset as an MLflow Input ---<br># MLflow can track datasets as inputs, which helps with lineage.<br>print(f&quot;\nLogging dataset as MLflow input. Target column: &#39;{target_column_name}&#39;&quot;)<br>try:<br>    mlflow_dataset = mlflow.data.from_pandas(<br>        df_raw,<br>        source=adult_dataset_info.metadata.get(&#39;data_url&#39;, &#39;UCI Repository ID 2&#39;),<br>        name=&quot;UCI Adult Income - Raw Combined&quot;,<br>        targets=target_column_name<br>    )<br>    mlflow.log_input(mlflow_dataset, context=&quot;raw_dataset&quot;) # context is a tag<br>    print(&quot;Dataset logged as MLflow input.&quot;)<br>    mlflow.log_param(&quot;raw_dataset_logged_as_input&quot;, &quot;success&quot;)<br>except Exception as e:<br>    print(f&quot;Error logging dataset as MLflow input: {e}&quot;)<br>    mlflow.log_param(&quot;raw_dataset_logged_as_input&quot;, &quot;failed&quot;)<br>    mlflow.log_param(&quot;raw_dataset_logging_error&quot;, str(e))<br><br># --- 5. Save and Log Dataset Metadata as Artifacts ---<br># The fetched dataset has metadata. Let&#39;s save this as a JSON artifact.<br># This is handy for understanding where the data came from, variable descriptions, etc.<br><br># Create a local directory to temporarily store artifacts before logging<br># This is good practice if you have multiple files or want to organize them.<br>local_artifacts_dir = &quot;temp_data_ingestion_artifacts&quot;<br>os.makedirs(local_artifacts_dir, exist_ok=True)<br><br># Save original UCI metadata<br>uci_metadata_file_path = os.path.join(local_artifacts_dir, &quot;uci_adult_dataset_metadata.json&quot;)<br>try:<br>    with open(uci_metadata_file_path, &quot;w&quot;) as f:<br>        json.dump(adult_dataset_info.metadata, f, indent=4)<br>    mlflow.log_artifact(uci_metadata_file_path, artifact_path=&quot;dataset_description&quot;)<br>    print(f&quot;\nUCI dataset metadata logged to MLflow artifacts: {uci_metadata_file_path}&quot;)<br>except Exception as e:<br>    print(f&quot;Error logging UCI metadata: {e}&quot;)<br><br># Save variable information from UCI<br>uci_variables_file_path = os.path.join(local_artifacts_dir, &quot;uci_adult_dataset_variables.json&quot;)<br>try:<br>    # Convert DataFrame to dict for JSON serialization if it&#39;s a DataFrame<br>    variables_info_serializable = adult_dataset_info.variables.to_dict(orient=&#39;records&#39;) if isinstance(adult_dataset_info.variables, pd.DataFrame) else adult_dataset_info.variables<br>    with open(uci_variables_file_path, &quot;w&quot;) as f:<br>        json.dump(variables_info_serializable, f, indent=4)<br>    mlflow.log_artifact(uci_variables_file_path, artifact_path=&quot;dataset_description&quot;)<br>    print(f&quot;UCI dataset variable info logged to MLflow artifacts: {uci_variables_file_path}&quot;)<br>except Exception as e:<br>    print(f&quot;Error logging UCI variable info: {e}&quot;)<br><br># --- 6. Extract and Log Unique Values for Categorical Features ---<br># Knowing unique values in categorical columns is key for EDA and preprocessing.<br>unique_values_categorical = {}<br># Select columns that are &#39;object&#39; (strings) or &#39;category&#39; type<br>categorical_cols = df_raw.select_dtypes(include=[&#39;object&#39;, &#39;category&#39;]).columns.tolist()<br><br># Make sure the target column isn&#39;t accidentally included if it&#39;s an object type<br>if target_column_name in categorical_cols:<br>    categorical_cols.remove(target_column_name)<br>    <br>for col in categorical_cols:<br>    # df_raw[col].dropna().unique() ensures we don&#39;t include NaN if it&#39;s treated as a category<br>    unique_values_categorical[col] = [str(val) for val in df_raw[col].dropna().unique().tolist()]<br><br>unique_values_file_path = os.path.join(local_artifacts_dir, &quot;categorical_features_unique_values.json&quot;)<br>try:<br>    with open(unique_values_file_path, &quot;w&quot;) as f:<br>        json.dump(unique_values_categorical, f, indent=4)<br>    mlflow.log_artifact(unique_values_file_path, artifact_path=&quot;dataset_description&quot;)<br>    print(f&quot;Unique values for categorical features logged to MLflow artifacts: {unique_values_file_path}&quot;)<br>except Exception as e:<br>    print(f&quot;Error logging unique categorical values: {e}&quot;)<br><br># --- 7. Log Dataset Overview Parameters ---<br># Let&#39;s log some general stats about the dataset as MLflow parameters.<br># This gives a quick overview in the MLflow UI for this run.<br>print(&quot;\nLogging dataset overview parameters to MLflow...&quot;)<br>try:<br>    dataset_params = {<br>        &quot;dataset_name&quot;: &quot;UCI Adult Income&quot;,<br>        &quot;source_repository_id&quot;: adult_dataset_info.metadata.get(&#39;uci_id&#39;, 2),<br>        &quot;num_rows_raw&quot;: df_raw.shape[0],<br>        &quot;num_columns_raw_total&quot;: df_raw.shape[1],<br>        &quot;num_features_raw&quot;: X_raw.shape[1],<br>        &quot;num_target_columns_raw&quot;: y_raw.shape[1],<br>        &quot;column_names_raw&quot;: df_raw.columns.tolist(),<br>        &quot;numerical_columns_count_raw&quot;: df_raw.select_dtypes(include=&#39;number&#39;).shape[1],<br>        &quot;categorical_columns_count_raw&quot;: len(categorical_cols),<br>        &quot;categorical_column_names_raw&quot;: categorical_cols,<br>        &quot;numerical_column_names_raw&quot;: df_raw.select_dtypes(include=&#39;number&#39;).columns.tolist(),<br>        &quot;missing_values_total_raw&quot;: int(df_raw.isnull().sum().sum()),<br>        &quot;target_column_name&quot;: target_column_name,<br>        &quot;target_unique_values_count&quot;: df_raw[target_column_name].nunique(),<br>        &quot;target_unique_values_list&quot;: [str(val) for val in df_raw[target_column_name].unique().tolist()],<br>        &quot;target_value_counts&quot;: {str(k): v for k, v in df_raw[target_column_name].value_counts().to_dict().items()},<br>        &quot;target_value_counts_percentage&quot;: {str(k): v for k, v in df_raw[target_column_name].value_counts(normalize=True).to_dict().items()}<br>    }<br>    # MLflow parameters have a length limit (often 250 chars for value), so for long lists, consider logging as a text artifact.<br>    # For this example, we&#39;ll try logging directly.<br>    for key, value in dataset_params.items():<br>        if isinstance(value, (list, dict)):<br>            # Convert lists/dicts to string for parameters, or log them as JSON artifacts if too long/complex<br>            try:<br>                mlflow.log_param(key, json.dumps(value))<br>            except TypeError: # handles cases where json.dumps might fail for complex objects<br>                 mlflow.log_param(key, str(value))<br><br>        else:<br>            mlflow.log_param(key, value)<br>    print(&quot;Dataset overview parameters logged.&quot;)<br>except Exception as e:<br>    print(f&quot;Error logging dataset parameters: {e}&quot;)<br><br># --- 8. Log a Sample of the Raw Data as an Artifact (e.g., CSV) ---<br># This can be useful for quick inspection from the MLflow UI.<br># Be mindful of data size; log a small sample if the dataset is large.<br>sample_df_path = os.path.join(local_artifacts_dir, &quot;raw_data_sample.csv&quot;)<br>try:<br>    df_raw.head(100).to_csv(sample_df_path, index=False) # Log first 100 rows<br>    mlflow.log_artifact(sample_df_path, artifact_path=&quot;dataset_samples&quot;)<br>    print(f&quot;Logged a sample of raw data to MLflow artifacts: {sample_df_path}&quot;)<br>except Exception as e:<br>    print(f&quot;Error logging data sample: {e}&quot;)<br><br># Clean up temporary local artifacts directory (optional)<br># import shutil<br># shutil.rmtree(local_artifacts_dir)<br># print(f&quot;Removed temporary local artifacts directory: {local_artifacts_dir}&quot;)<br><br>print(f&quot;\nData ingestion and initial preparation run finished. Check MLflow UI for run ID: {data_ingestion_run.info.run_id}&quot;)<br><br># --- End the current MLflow run ---<br>if mlflow.active_run():<br>    mlflow.end_run()<br>    print(&quot;MLflow run for Data Ingestion ended.&quot;)</pre><blockquote><strong>A Quick Detour: Solving Potential Artifact Upload Issues (AuthorizationPermissionMismatch)</strong></blockquote><p>As you run the data ingestion code, especially the parts that use mlflow.log_artifact() (like logging metadata files or data samples), you might run into an error message that looks something like this:</p><pre>ErrorCode:AuthorizationPermissionMismatch<br>Content: &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;Error&gt;&lt;Code&gt;AuthorizationPermissionMismatch&lt;/Code&gt;&lt;Message&gt;This request is not authorized to perform this operation using this permission.<br>RequestId:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx<br>Time:YYYY-MM-DDTHH:MM:SS.SSSSSSSZ&lt;/Message&gt;&lt;/Error&gt;</pre><p>This <strong><em>AuthorizationPermissionMismatch</em></strong> is a common hiccup when you’re starting out with MLflow and a cloud-based artifact store like Azure Blob Storage. Let’s understand why it happens and how to fix it.</p><p><strong>Why Does This Happen? The Client vs. Server Distinction</strong></p><p>When you set up the MLflow server using the Helm chart, you provided an Azure Storage access key in the mlflow-values.yaml file. This key allows the <strong>MLflow server itself</strong> to:</p><ul><li>Read artifacts (so it can show them in the UI).</li><li>Delete artifacts (if you delete a run or an artifact from the UI).</li></ul><p>However, when your Python script (running in your Jupyter notebook or on your local machine) executes mlflow.log_artifact(), the <strong>MLflow client library in your script</strong> tries to upload the artifact <em>directly</em> to Azure Blob Storage. It doesn’t send the file through the MLflow server for uploading.</p><p>Therefore, the identity running your Python script needs separate write permissions to the Azure Storage Account and container. The access key configured for the MLflow server doesn’t automatically grant these permissions to your client environment.</p><p><strong>Resolving Artifact Upload Permissions</strong></p><p>There are a few ways to grant your client environment the necessary permissions, depending on where your code is running. The goal is to give the identity performing the upload the <strong>“Storage Blob Data Contributor”</strong> role on your Azure Storage Account. This role provides the permissions needed to read, write, and delete blobs.</p><p>Let’s look at three common scenarios:</p><h4>Option 1: For Local Development (Your Computer, using Azure CLI Login)</h4><p>If you’re running your notebooks or scripts directly on your local machine and you’re logged into Azure using the Azure CLI (az login), this is often the quickest way to get started. You’ll assign the “Storage Blob Data Contributor” role to your currently signed-in Azure user, scoped to the specific storage account MLflow is using.</p><ul><li><strong>Important Note on Roles:</strong> Even if your Azure user account is an “Owner” or “Contributor” at the subscription level, this doesn’t automatically grant data-level permissions like “Storage Blob Data Contributor” on individual storage accounts. Azure separates management plane roles (like Owner) from data plane roles for finer-grained security. So, an explicit assignment on the storage account is usually necessary.</li></ul><p>The “Storage Blob Data Contributor” role is a data plane role. Even as a subscription Owner, you might not inherently have this specific data access permission directly on the storage account’s data. An explicit role assignment for data access on the storage account itself is often necessary for client-side tools.</p><p><strong>You can often see this in the Azure Portal:</strong></p><ol><li>Navigate to your Azure Storage Account (e.g., tharindumlflowxxxxxxxx).</li><li>In the left-hand menu, select <strong>Access control (IAM)</strong>.</li><li>Go to the <strong>Role assignments</strong> tab.</li><li>You might see your user account listed with the “Owner” role, but if you look at the “Scope” column, it might say “Subscription (Inherited)” or something similar, indicating the ownership is at a higher level. This inherited management-level role doesn’t automatically grant the specific “Storage Blob Data Contributor” permission needed for your client script to write blobs directly.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2nzoecbdxg_RYgt8IQ8q4g.png" /><figcaption><strong>Verifying role assignments in the Azure Portal: Inherited subscription-level ‘Owner’ roles might not grant direct data plane access to blobs without an explicit ‘Storage Blob Data Contributor’ assignment on the storage account itself.</strong></figcaption></figure><p>Here’s how to assign the role using Azure CLI:</p><p><strong>Ensure Environment Variables are Set:</strong><br>Make sure your RESOURCE_GROUP and STORAGE_ACCOUNT_NAME environment variables are set to the values you used when creating these resources. If you’re in a new terminal session, you might need to export them again:</p><pre>export RESOURCE_GROUP=&quot;tharindu-mlflow-rg&quot;<br># Use the actual unique name of your storage account<br>export STORAGE_ACCOUNT_NAME=&quot;tharindumlflowxxxxxxxx&quot; # Replace xxxxxxxx with your unique suffixexport RESOURCE_GROUP=&quot;tharindu-mlflow-rg&quot; # Use the actual unique name of your storage account export STORAGE_ACCOUNT_NAME=&quot;tharindumlflowxxxxxxxx&quot; # Replace xxxxxxxx with your unique suffix<br><br>export STORAGE_ACCOUNT_NAME=&quot;tharindumlflow0aa3981a&quot; # Replace xxxxxxxx with your unique suffixexport RESOURCE_GROUP=&quot;tharindu-mlflow-rg&quot; # Use the actual unique name of your storage account export STORAGE_ACCOUNT_NAME=&quot;tharindumlflowxxxxxxxx&quot; # Replace xxxxxxxx with your unique suffix</pre><p><strong>Get Your Signed-in User’s Object ID and Subscription ID:</strong><br>The Azure CLI needs the unique Object ID of your user in Azure Active Directory and your current subscription ID to correctly scope the role assignment.</p><pre>AZURE_USER_OBJECT_ID=$(az ad signed-in-user show --query id -o tsv)<br>if [ -z &quot;$AZURE_USER_OBJECT_ID&quot; ]; then<br>    echo &quot;Error: Could not retrieve Object ID for the signed-in user. Please ensure you are logged in with &#39;az login&#39;.&quot;<br>    # You might want to exit or handle this error if scripting<br>else<br>    echo &quot;Current User Object ID: $AZURE_USER_OBJECT_ID&quot;<br>fi<br><br>SUBSCRIPTION_ID=$(az account show --query id -o tsv)<br>if [ -z &quot;$SUBSCRIPTION_ID&quot; ]; then<br>    echo &quot;Error: Could not retrieve Subscription ID.&quot;<br>else<br>    echo &quot;Current Subscription ID: $SUBSCRIPTION_ID&quot;<br>fi</pre><p><strong>Construct the Scope for the Role Assignment:</strong><br>The scope defines exactly which resource the role will apply to — in this case, your specific storage account.</p><pre>SCOPE=&quot;/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT_NAME&quot;<br>echo &quot;Role assignment will be applied to scope: $SCOPE&quot;</pre><p><strong>Assign the “Storage Blob Data Contributor” Role: </strong>Now, execute the role assignment command.</p><pre>az role assignment create \<br>    --assignee-object-id &quot;$AZURE_USER_OBJECT_ID&quot; \<br>    --assignee-principal-type User \<br>    --role &quot;Storage Blob Data Contributor&quot; \<br>    --scope &quot;$SCOPE&quot;</pre><p>If successful, this command will output a JSON object confirming the role assignment. It might look something like this:</p><pre>{<br>  &quot;condition&quot;: null,<br>  &quot;conditionVersion&quot;: null,<br>  // ... other details ...<br>  &quot;principalId&quot;: &quot;your-user-object-id&quot;,<br>  &quot;principalType&quot;: &quot;User&quot;,<br>  &quot;roleDefinitionId&quot;: &quot;/subscriptions/your-subscription-id/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe&quot;, // ID for Storage Blob Data Contributor<br>  &quot;scope&quot;: &quot;/subscriptions/your-subscription-id/resourceGroups/your-rg/providers/Microsoft.Storage/storageAccounts/your-storage-account&quot;,<br>  // ... other details ...<br>}</pre><p>Once these steps are completed, navigate back to the <strong>IAM</strong> section of the storage account. Under <strong>Role Assignments</strong>, refresh the page — you should now see the correct role assignment displayed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rz9Sk_sxpkhD2v33-22IjQ.png" /><figcaption>Storage Blob Data Contributor Role Assignment</figcaption></figure><p><strong>Refresh Your Local Azure CLI Token (Important!):</strong><br>Role assignments can take a few minutes to propagate through Azure’s systems. To ensure your local Azure CLI session picks up the new permissions, it’s a good idea to log out and log back in:</p><pre>az logout <br><br>az login --use-device-code</pre><p>Follow the on-screen instructions to complete the login.</p><p><strong>Retry Your Python Script: </strong>After waiting a few minutes and re-authenticating, try running your Python script’s data ingestion cell again. The mlflow.log_artifact() calls should now succeed. Also ensure the azure-identity Python package is installed in your environment (pip install azure-identity or poetry add azure-identity), as MLflow uses it for Azure authentication.</p><blockquote>(Self-reflection during my own setup: I initially tried using just the User Principal Name (UPN, usually your email) for the — assignee flag, but sometimes Azure can be particular. Using — assignee-object-id along with — assignee-principal-type User is a more robust way to uniquely identify the user in Azure AD for role assignments.)</blockquote><h4>Option 2: For Code Running on Azure Compute (e.g., Azure VM, Azure ML Compute Instance)</h4><p>If your Jupyter notebook or script is running <em>within</em> an Azure environment like an Azure Virtual Machine or an Azure Machine Learning Compute Instance, the best practice is to use a <strong>Managed Identity</strong>.</p><ul><li><strong>Concept:</strong> You assign an identity (either system-assigned or user-assigned) to your Azure compute resource. Then, you grant this Managed Identity the “Storage Blob Data Contributor” role on your storage account. Code running on that compute resource can then automatically use this identity to authenticate to Azure services without needing any explicit credentials in your code or environment variables.</li></ul><p><strong>How to implement:</strong></p><ol><li>Create a User-Assigned Managed Identity (UAMI) or enable System-Assigned Managed Identity on your Azure compute resource.</li><li>Grant this Managed Identity the “Storage Blob Data Contributor” role on the target Azure Storage Account (similar to the role assignment for a user, but you’d use the Managed Identity’s Principal ID as the assignee).</li><li>Ensure the azure-identity Python package is installed in your compute environment. Your Python script will then typically use DefaultAzureCredential, which automatically detects and uses the Managed Identity.</li></ol><p>(For brevity in this section, we won’t detail all UAMI commands here, but the principle is the same: assign the role to the identity your code will run as.)</p><h4>Option 3: Using an Azure Service Principal (Recommended for Automation/Non-Azure Compute)</h4><p>If your Jupyter notebooks or scripts are running outside of Azure (e.g., on your local machine for development, Google Colab, AWS SageMaker, or as part of an automated CI/CD pipeline), you can’t use Azure Managed Identities or rely on an interactive az login session for authentication. In this case, creating and using an Azure Service Principal (SP) is the recommended approach.</p><p><strong>Understanding the Identity Components (Simplified)</strong></p><p>Before we dive into the commands, let’s briefly understand the key pieces we’ll create in Azure Entra ID (Microsoft’s identity service, formerly Azure AD):</p><ol><li><strong>Application Registration:</strong> Think of this as defining a <em>blueprint</em> or <em>identity profile</em> for your application (our Python script needing access). It’s like registering the concept of your app with Azure, saying “I have an application that needs to interact with Azure resources.”</li><li><strong>Service Principal:</strong> This is the actual <em>instance</em> or <em>identity</em> of your application created from that blueprint within your specific Azure environment (your “tenant”). This is the “robot user” or “actor” that will be assigned permissions to do things, like uploading files.</li><li><strong>Client Secret:</strong> This acts as the <em>password</em> for the Service Principal. Your application will use this secret, along with the Application (Client) ID, to prove its identity when it needs to access Azure resources.</li></ol><p>We’ll create these using the Azure CLI and then grant the Service Principal the necessary role.</p><p><strong>Step-by-Step: Creating and Using a Service Principal via Azure CLI</strong></p><p><strong>Prerequisites:</strong></p><ol><li><strong>Azure CLI Installed and Logged In:</strong> Ensure you have the Azure CLI installed and are logged in to your Azure account (az login — use-device-code). You need appropriate permissions in your Azure AD tenant to create app registrations and assign roles (e.g., “Application Administrator” or “Cloud Application Administrator” for creating the SP, and “Owner” or “User Access Administrator” on the storage account for assigning roles).</li><li><strong>Environment Variables for Resources:</strong> Make sure your shell has the necessary environment variables from earlier steps, or re-declare them:</li></ol><pre>export RESOURCE_GROUP=&quot;tharindu-mlflow-rg&quot;<br>export STORAGE_ACCOUNT_NAME=&quot;tharindumlflowXXXXXXXX&quot; # Use the *actual* unique name of your existing storage account<br>export SUBSCRIPTION_ID=$(az account show --query id -o tsv)<br><br># It&#39;s crucial to use the *actual* name of your storage account created earlier.<br># If you don&#39;t remember it, you can list storage accounts in your resource group:<br># az storage account list --resource-group $RESOURCE_GROUP --query &quot;[].name&quot; -o tsv<br># Then set it:<br># export STORAGE_ACCOUNT_NAME=&quot;your-actual-storage-account-name&quot;<br>echo &quot;Using Resource Group: $RESOURCE_GROUP&quot;<br>echo &quot;Using Storage Account: $STORAGE_ACCOUNT_NAME&quot;<br>echo &quot;Using Subscription ID: $SUBSCRIPTION_ID&quot;<br><br># Choose a Name for your Service Principal<br>export SP_NAME=&quot;mlflow-artifact-uploader-sp-$(openssl rand -hex 3)&quot; # Creates a unique name<br>echo &quot;Will create Service Principal named: $SP_NAME&quot;</pre><ol><li><strong>Create the Azure AD Application Registration: </strong>An application registration is the global representation of your application in Azure AD. <em>az ad app create — display-name “$SP_NAME”</em>: Creates the app registration.</li></ol><pre>APP_CREATE_OUTPUT=$(az ad app create --display-name &quot;$SP_NAME&quot;)<br>APP_ID=$(echo $APP_CREATE_OUTPUT | jq -r &#39;.appId&#39;)<br>echo $APP_ID</pre><p><strong>2. Create the Service Principal: </strong>The service principal is the local representation (instance) of the application in your tenant, which allows it to be assigned roles. <em>az ad sp create — id “$APP_ID”</em>: Creates the service principal for the application we just registered.</p><pre>SP_CREATE_OUTPUT=$(az ad sp create --id &quot;$APP_ID&quot;)<br>SP_OBJECT_ID=$(echo $SP_CREATE_OUTPUT | jq -r &#39;.id&#39;) <br>echo $SP_OBJECT_ID</pre><p><strong>3. Create a Client Secret for the Service Principal: </strong>The application will use this secret to prove its identity. <em>az ad app credential reset — id “$APP_ID” — append</em>: Generates a new secret (password) for the application.</p><pre>CLIENT_SECRET_OUTPUT=$(az ad app credential reset --id &quot;$APP_ID&quot; --append --display-name &quot;mlflow_client_secret&quot;)<br>CLIENT_SECRET=$(echo $CLIENT_SECRET_OUTPUT | jq -r &#39;.password&#39;)<br>echo $CLIENT_SECRET</pre><ul><li><strong>CRITICAL:</strong> The output of this command (password) is the only time you’ll see this secret. Copy it immediately and store it securely (e.g., in a password manager or temporarily until you add it to your .env file).</li></ul><p><strong>4. Assign the “Storage Blob Data Contributor” Role to the Service Principal: </strong>Now, grant the Service Principal permissions to write to your Azure Blob Storage container.</p><pre>AZURE_TENANT_ID=$(az account show --query tenantId -o tsv)<br>echo &quot;Azure Tenant ID: $AZURE_TENANT_ID&quot;<br><br>echo &quot;Constructing scope for role assignment...&quot;<br>STORAGE_ACCOUNT_RESOURCE_ID=&quot;/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT_NAME&quot;<br>echo &quot;Scope: $STORAGE_ACCOUNT_RESOURCE_ID&quot;<br><br>az role assignment create \<br>    --assignee-object-id &quot;$SP_OBJECT_ID&quot; \<br>    --assignee-principal-type ServicePrincipal \<br>    --role &quot;Storage Blob Data Contributor&quot; \<br>    --scope &quot;$STORAGE_ACCOUNT_RESOURCE_ID&quot;</pre><ul><li>— assignee-object-id “$SP_OBJECT_ID”: Specifies the Service Principal to grant the role to using its Object ID.</li><li>— assignee-principal-type ServicePrincipal: Clarifies the type of the assignee.</li><li>— role “Storage Blob Data Contributor”: The specific role that allows data operations on blobs.</li><li>— scope “$STORAGE_ACCOUNT_RESOURCE_ID”: Restricts this permission to <em>only</em> the specified storage account.</li></ul><p><strong>Summary of Credentials to Use:</strong><br>You now have the three pieces of information your application needs:</p><ul><li><strong>Client ID (App ID):</strong> $APP_ID (e.g., xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)</li><li><strong>Client Secret:</strong> $CLIENT_SECRET (e.g., YOUR_VERY_LONG_AND_COMPLEX_SECRET_VALUE)</li><li><strong>Tenant ID:</strong> $AZURE_TENANT_ID (e.g., yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy)</li></ul><p><strong>Step 5: Configure Your Local Environment (.env file): </strong>In the root directory of your Python project (or where your Jupyter notebook resides), update or create the .env file. Add the Service Principal credentials to it:</p><pre>echo $APP_ID<br>echo $CLIENT_SECRET<br>echo $AZURE_TENANT_ID</pre><p>Replace the placeholders &lt;YOUR_APP_ID_FROM_CLI&gt;, &lt;YOUR_CLIENT_SECRET_FROM_CLI&gt;, and &lt;YOUR_TENANT_ID_FROM_CLI&gt; with the actual values you obtained.</p><pre># .env file<br><br># MLflow UI Admin Username (configured in mlflow-values.yaml)<br>MLFLOW_TRACKING_USERNAME=admin<br><br># MLFLOW UI Admin Password (configured in mlflow-values.yaml - must be &gt;12 chars)<br>MLFLOW_TRACKING_PASSWORD=&quot;&lt;YOUR_MLFLOW_UI_ADMIN_PASSWORD&gt;&quot; # Replace with your actual UI admin password<br><br># MLflow Tracking Server URI<br>MLFLOW_TRACKING_URI=&quot;http://&lt;YOUR_EXTERNAL_IP&gt;&quot; # Replace with your actual MLflow server IP<br><br># --- Azure Service Principal Credentials for Artifact Upload ---<br># Replace placeholders with the values obtained from the Azure CLI steps above<br>AZURE_CLIENT_ID=&quot;&lt;YOUR_APP_ID_FROM_CLI&gt;&quot;<br>AZURE_CLIENT_SECRET=&quot;&lt;YOUR_CLIENT_SECRET_FROM_CLI&gt;&quot;<br>AZURE_TENANT_ID=&quot;&lt;YOUR_TENANT_ID_FROM_CLI&gt;&quot;</pre><p><strong>Important Security Note:</strong></p><ul><li>Add .env to your project’s .gitignore file to prevent accidentally committing these sensitive credentials to version control.</li><li>Treat the CLIENT_SECRET like a password. If it’s compromised, you should generate a new one in the Azure portal (App Registrations -&gt; Your App -&gt; Certificates &amp; secrets) and delete the old one.</li></ul><p><strong>Step 6: Using the Service Principal in Your Python Code (Implicitly): </strong>Your Python code for connecting to MLflow and logging artifacts doesn’t need to change significantly. The key is that the environment variables (AZURE_CLIENT_ID, etc.) must be set <em>before</em> MLflow attempts to interact with Azure Blob Storage for artifact logging.</p><p>When mlflow.log_artifact() is called, the MLflow client (which uses the Azure SDK for Python for Azure Blob Storage operations) will internally use a credential chain that includes checking for these environment variables. If found, it authenticates as the Service Principal, which now has the “Storage Blob Data Contributor” role, allowing it to upload the artifact.</p><p><strong>Step 7: Verification in the Azure Portal (Optional but Recommended)</strong></p><p><strong>Verify App Registration:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HCe4rbPEaufztgoc_O3dpA.png" /><figcaption><em>Verifying the App Registration and Client ID in Azure Entra ID</em></figcaption></figure><ul><li>Go to the Azure portal.</li><li>Search for and select “Azure Entra ID.”</li><li>Under “Manage,” click on “App registrations.”</li><li>Select the “All applications” tab and search for the name you gave your SP (e.g., mlflow-artifact-uploader-sp-xxxxxx). You should see it listed.</li><li>Click on it to see its details (like Application (client) ID).</li><li>Under “Manage” for this app, you can go to “Certificates &amp; secrets” to see the secret’s display name and expiry (but not the value).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YwRrKnBDTzq5C3YytLr48Q.png" /><figcaption><em>Confirming the Client Secret exists within the App Registration</em></figcaption></figure><p><strong>Verify Role Assignment on Storage Account:</strong></p><ul><li>Navigate to your Azure Storage Account (e.g., tharindumlflowxxxxxxxx).</li><li>In the left-hand menu, select “Access control (IAM).”</li><li>Go to the “Role assignments” tab.</li><li>Filter or search for your Service Principal’s name. You should see it listed with the “Storage Blob Data Contributor” role assigned to it, scoped to “This resource.”</li><li>(It might take a few minutes for role assignments to fully propagate and appear in the portal UI after CLI execution).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HPkQ4V7-l0hxZiUtiIIsBg.png" /><figcaption><em>Verifying the ‘Storage Blob Data Contributor’ role assignment for the Service Principal on the Storage Account</em></figcaption></figure><p>With the Service Principal correctly configured and its credentials in your .env file, your Python scripts should now be able to authenticate and upload artifacts to Azure Blob Storage without encountering the AuthorizationPermissionMismatch error. This method is secure and suitable for automated workflows.</p><p>Now we can verify this in the MLflow UI as follows. The following includes a couple of screenshots and explains what we can see in the UI and the screenshots.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4r1BC4MWNMQGKrmoHPo3bA.png" /><figcaption><strong>MLflow Run Overview &amp; Dataset Input</strong></figcaption></figure><ul><li><strong>Run Identification:</strong> The screenshot shows the run named “Data_Ingestion_and_Initial_Prep” within the “UCI Adult Income Prediction — Centralized” experiment, corresponding to the run started by mlflow.start_run(run_name=”Data_Ingestion_and_Initial_Prep”). The Run ID, Experiment ID, and Status (“Finished” because mlflow.end_run() was called) are clearly visible.</li><li><strong>Dataset Input Logging:</strong> The key section here is “Datasets used”. It shows “UCI Adult Income — Raw Combined”, which was logged via mlflow.log_input(mlflow_dataset, context=”raw_dataset”) in step 4. Clicking “Open dataset” would show the metadata logged (name, source URL, target column, digest/hash, row/feature counts). This confirms the dataset lineage tracking is working.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gw_DCv95B9SC4dbTS-nxzA.png" /><figcaption><strong>Parameter Logging</strong></figcaption></figure><p><strong>Parameter Logging:</strong> This screenshot displays the “Parameters” section, directly reflecting the numerous calls to mlflow.log_param() primarily from step 7, but also steps 1, 3, and 4.</p><p><strong>Examples:</strong></p><ul><li>num_rows_raw: Logged from the dataset_params dictionary in step 7.</li><li>int_columns_converted_to_float: Logged in step 3.</li><li>raw_dataset_logged_as_input: Logged in step 4.</li><li>target_value_counts: Logged from dataset_params, showing the JSON string conversion of the dictionary.</li><li>categorical_column_names_raw: Logged from dataset_params, showing the list converted to a string.</li></ul><p><strong>Value:</strong> This section provides a quick, high-level summary of the run’s configuration and the input data’s characteristics without needing to download any files.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WPAYv4Dugp8xc6x3XqnzwQ.png" /><figcaption><strong>Artifact Logging</strong></figcaption></figure><ul><li><strong>Artifact Logging:</strong> This view shows the files stored as artifacts for this run, corresponding to the mlflow.log_artifact() calls in steps 5, 6, and 8.</li><li><strong>Folder Structure:</strong> The artifact_path argument in mlflow.log_artifact() created the folder structure seen: dataset_description and dataset_samples.</li><li><strong>Files Logged:</strong></li><li>Inside dataset_description: We see uci_adult_dataset_metadata.json, uci_adult_dataset_variables.json, and categorical_features_unique_values.json, exactly as logged in steps 5 and 6. The UI even provides a preview of the JSON content.</li><li>Inside dataset_samples: We see raw_data_sample.csv logged in step 8.</li><li><strong>Value:</strong> Artifacts store larger, more detailed outputs. Here, they preserve the exact metadata files from the source and a sample of the data used, which is invaluable for deep dives and debugging later. The path shown (wasbs://artifactroot@…) confirms the artifacts are stored in our configured Azure Blob Storage container.</li></ul><h3>Phase 2: Exploratory Data Analysis (EDA) with MLflow Artifact Logging</h3><p>Exploratory Data Analysis is a crucial step to understand the dataset’s characteristics, distributions, correlations, and potential issues. A key part of EDA involves generating visualizations. Traditionally, these plots might live temporarily in a notebook’s output or be manually saved to local folders. With MLflow, we can automatically log these plots as artifacts directly associated with a specific experiment run, preserving the visual insights alongside the code and parameters that generated them.</p><p>Let’s start a new MLflow run specifically for our EDA phase. This helps keep the logs organized.</p><pre># --- Notebook Cell: Start EDA Run ---<br># Ensure MLflow tracking URI and credentials are set from the initial setup cell<br># Assumes &#39;df_raw&#39; DataFrame is available from the previous Data Ingestion phase.<br># Assumes &#39;target_column_name&#39; and &#39;categorical_cols&#39; were defined earlier.<br><br># If a previous run is active, end it first.<br>if mlflow.active_run():<br>    mlflow.end_run()<br>    print(&quot;Ended previous active run.&quot;)<br><br># Start a new run dedicated to EDA<br>eda_run = mlflow.start_run(run_name=&quot;Exploratory_Data_Analysis&quot;)<br>print(f&quot;Starting new MLflow Run for EDA: {eda_run.data.tags.get(&#39;mlflow.runName&#39;)}&quot;)<br>print(f&quot;Run ID: {eda_run.info.run_id}&quot;)<br><br># For convenience, let&#39;s use a shorter variable name for our DataFrame in this phase<br>df = df_raw # Or df = df_cleaned if you performed cleaning steps<br><br># Log a parameter indicating the data state used for EDA<br>mlflow.log_param(&quot;eda_data_source&quot;, &quot;raw_combined&quot;) # Or &quot;cleaned&quot; if applicable</pre><p>Now, let’s generate various plots and log them using mlflow.log_figure(). This function takes the current Matplotlib figure (plt.gcf()) and saves it as an image artifact to the specified path within the run’s artifact store (our Azure Blob Storage).</p><ol><li><strong>Visualizing Numerical Feature Distributions (Histograms)</strong></li></ol><pre># --- Notebook Cell: Numerical Histograms ---<br>import matplotlib.pyplot as plt<br>import seaborn as sns<br>import mlflow<br><br>print(&quot;Generating and logging histograms for numerical features...&quot;)<br>sns.set(style=&quot;whitegrid&quot;)<br><br># Select only numerical columns (excluding potential IDs if necessary)<br>numerical_cols = df.select_dtypes(include=[&#39;int64&#39;, &#39;float64&#39;]).columns<br>mlflow.log_param(&quot;numerical_features_for_eda&quot;, list(numerical_cols))<br><br>for col in numerical_cols:<br>    plt.figure(figsize=(8, 4))<br>    sns.histplot(df[col], bins=30, kde=True, color=&#39;skyblue&#39;, edgecolor=&#39;black&#39;)<br>    plt.title(f&quot;Distribution of {col}&quot;, fontsize=14, fontweight=&#39;bold&#39;)<br>    plt.xlabel(col)<br>    plt.ylabel(&quot;Frequency&quot;)<br>    plt.tight_layout()<br><br>    # Log the figure directly to MLflow under a specific sub-directory<br>    # Using f-strings makes organizing artifacts easy<br>    artifact_path = f&quot;eda_plots/numerical_distributions/hist_{col}.png&quot;<br>    mlflow.log_figure(plt.gcf(), artifact_path)<br>    print(f&quot;  Logged histogram: {artifact_path}&quot;)<br><br>    # plt.show() # Display the plot in the notebook (optional)<br>    plt.close() # Close the figure to free memory, crucial in loops!<br><br>print(&quot;Finished logging histograms.&quot;)</pre><ul><li><strong>mlflow.log_figure(plt.gcf(), artifact_path)</strong>: This is the core command. plt.gcf() gets the current Matplotlib figure, and artifact_path defines where it will be stored within the MLflow run’s artifacts (e.g., eda_plots/numerical_distributions/hist_age.png).</li><li><strong>plt.close()</strong>: Closing the figure after logging is important, especially within loops, to prevent plots from consuming excessive memory or interfering with subsequent plots.</li></ul><p><strong>2. Visualizing Numerical Feature Distributions (Boxplots)</strong></p><p>Boxplots help identify outliers and understand the spread.</p><pre># --- Notebook Cell: Numerical Boxplots ---<br>print(&quot;Generating and logging boxplots for numerical features...&quot;)<br><br>for col in numerical_cols: # Reuse numerical_cols from previous cell<br>    plt.figure(figsize=(8, 4))<br>    sns.boxplot(x=df[col], color=&#39;lightgreen&#39;, linewidth=1.5)<br>    plt.title(f&quot;Boxplot for {col}&quot;, fontsize=14, fontweight=&#39;bold&#39;)<br>    plt.xlabel(col)<br>    plt.tight_layout()<br><br>    # Log the figure directly to MLflow<br>    artifact_path = f&quot;eda_plots/numerical_distributions/boxplot_{col}.png&quot;<br>    mlflow.log_figure(plt.gcf(), artifact_path)<br>    print(f&quot;  Logged boxplot: {artifact_path}&quot;)<br><br>    # plt.show() # Optional display<br>    plt.close()<br><br>print(&quot;Finished logging boxplots.&quot;)</pre><p><strong>3. Visualizing Numerical Feature Correlations (Heatmap)</strong></p><p>Understanding correlations is vital for feature selection and modeling.</p><pre># --- Notebook Cell: Correlation Heatmap ---<br>print(&quot;Generating and logging correlation heatmap...&quot;)<br><br>plt.figure(figsize=(12, 10))<br># Recalculate correlation matrix on the numerical columns identified<br>corr_matrix = df[numerical_cols].corr()<br>sns.heatmap(<br>    corr_matrix, annot=True, cmap=&#39;coolwarm&#39;, fmt=&quot;.2f&quot;, linewidths=0.5,<br>    square=True, cbar_kws={&quot;shrink&quot;: .8}<br>)<br>plt.title(&quot;Correlation Heatmap of Numerical Features&quot;, fontsize=16, fontweight=&#39;bold&#39;)<br>plt.tight_layout()<br><br># Log the figure directly to MLflow<br>artifact_path = &quot;eda_plots/correlation_heatmap.png&quot;<br>mlflow.log_figure(plt.gcf(), artifact_path)<br>print(f&quot;  Logged heatmap: {artifact_path}&quot;)<br><br># plt.show() # Optional display<br>plt.close()<br><br>print(&quot;Finished logging correlation heatmap.&quot;)</pre><p><strong>4. Visualizing Categorical Feature Distributions</strong></p><p>Let’s see the counts for each category in our categorical features.</p><pre># --- Notebook Cell: Categorical Distributions ---<br>import matplotlib.pyplot as plt # Ensure imported if in new session<br>import seaborn as sns         # Ensure imported<br>import mlflow               # Ensure imported<br><br>print(&quot;Generating and logging distributions for categorical features...&quot;)<br>sns.set(style=&quot;whitegrid&quot;)<br><br># Ensure &#39;categorical_cols&#39; is defined (likely from data ingestion phase)<br># Example: categorical_cols = df.select_dtypes(include=[&#39;object&#39;, &#39;category&#39;]).columns.tolist()<br># Make sure the target column isn&#39;t included if it&#39;s categorical<br># if target_column_name in categorical_cols:<br>#     categorical_cols.remove(target_column_name)<br><br>mlflow.log_param(&quot;categorical_features_for_eda&quot;, list(categorical_cols))<br><br><br>for col in categorical_cols:<br>    plt.figure(figsize=(10, 5))<br>    # Using hue=col helps if colors are needed per bar, otherwise optional<br>    sns.countplot(<br>        data=df,<br>        y=col, # Using y can be better for long category names<br>        order=df[col].value_counts().index, # Order by frequency<br>        palette=&quot;viridis&quot;,<br>        hue=col, # Assign hue for color mapping, necessary if legend=False fails<br>        legend=False # Avoid redundant legend if hue=col<br>    )<br>    plt.title(f&quot;Distribution of {col}&quot;, fontsize=14, fontweight=&#39;bold&#39;)<br>    plt.xlabel(&quot;Count&quot;, fontsize=12)<br>    plt.ylabel(col, fontsize=12)<br>    # plt.xticks(rotation=45) # Not needed if using y=col<br>    plt.tight_layout()<br><br>    # Log the figure directly to MLflow<br>    artifact_path = f&quot;eda_plots/categorical_distributions/dist_{col}.png&quot;<br>    mlflow.log_figure(plt.gcf(), artifact_path)<br>    print(f&quot;  Logged categorical distribution: {artifact_path}&quot;)<br><br>    # plt.show() # Optional display<br>    plt.close()<br><br>print(&quot;Finished logging categorical distributions.&quot;)</pre><p><strong>5. Visualizing Categorical Features vs. Target Variable</strong></p><p>Understanding how categorical features relate to the target variable (income) is key for predictive modeling.</p><pre># --- Notebook Cell: Categorical vs Target ---<br>print(&quot;Generating and logging categorical features vs target...&quot;)<br>sns.set(style=&quot;whitegrid&quot;)<br><br># Ensure &#39;target_column_name&#39; is defined (e.g., &#39;income&#39;)<br># target_col = target_column_name<br>target_col = &#39;income&#39; # Explicitly set if not using variable from ingestion<br><br>if target_col in df.columns:<br>    mlflow.log_param(&quot;target_column_for_eda_comparison&quot;, target_col)<br>    for col in categorical_cols: # Reuse categorical_cols<br>        plt.figure(figsize=(12, 6)) # Adjusted size<br>        sns.countplot(<br>            data=df,<br>            y=col, # Again, using y=col often better<br>            hue=target_col, # Color bars based on income level<br>            order=df[col].value_counts().index, # Order categories by frequency<br>            palette=&quot;Set1&quot; # Use a different palette for contrast<br>        )<br>        plt.title(f&quot;{col} vs {target_col}&quot;, fontsize=14, fontweight=&#39;bold&#39;)<br>        plt.xlabel(&quot;Count&quot;, fontsize=12)<br>        plt.ylabel(col, fontsize=12)<br>        plt.legend(title=target_col, loc=&#39;center right&#39;, bbox_to_anchor=(1.25, 0.5)) # Adjust legend position<br>        plt.tight_layout(rect=[0, 0, 1, 1]) # Adjust layout to potentially make space for legend<br><br>        # Log the figure directly to MLflow<br>        artifact_path = f&quot;eda_plots/categorical_vs_target/{col}_vs_{target_col}.png&quot;<br>        mlflow.log_figure(plt.gcf(), artifact_path)<br>        print(f&quot;  Logged categorical vs target: {artifact_path}&quot;)<br><br>        # plt.show() # Optional display<br>        plt.close()<br>else:<br>    print(f&quot;Target column &#39;{target_col}&#39; not found in DataFrame. Skipping comparison plots.&quot;)<br>    mlflow.log_param(&quot;target_comparison_skipped&quot;, f&quot;Column &#39;{target_col}&#39; not found.&quot;)<br><br>print(&quot;Finished logging categorical vs target plots.&quot;)<br><br># --- End the EDA Run ---<br>if mlflow.active_run():<br>    mlflow.end_run()<br>    print(&quot;MLflow run for EDA ended.&quot;)</pre><p><strong>Viewing EDA Artifacts in MLflow UI</strong></p><p>After running these cells, navigate back to your MLflow UI in the browser (http://&lt;YOUR_EXTERNAL_IP&gt;).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hyyAb_k9lH9JFqkBsEraRw.png" /><figcaption>The experiment (UCI Adult Income Prediction — Centralized)</figcaption></figure><ol><li>Find the experiment (UCI Adult Income Prediction — Centralized).</li><li>Click on the latest run, which should have the name “Exploratory_Data_Analysis” (or similar).</li><li>Scroll down to the “Artifacts” section.</li><li>You will see a folder structure mirroring the artifact_path you specified (e.g., eda_plots/).</li><li>Click through the folders (numerical_distributions, categorical_distributions, etc.) to view the saved PNG images of your plots directly in the browser.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6KMgSBOJ_FMXsoTjoJsXRA.png" /><figcaption><em>MLflow UI view showing logged EDA plot artifacts</em></figcaption></figure><h3>Conclusion: Your Foundation for Advanced ML Workflows</h3><h4>What We’ve Accomplished</h4><p>In this article, we’ve established a comprehensive foundation for systematic ML experiment tracking that addresses the core challenges identified in Part 1:</p><p>✅ <strong>Connected Development to Production</strong>: Seamless integration between local development and centralized MLflow infrastructure</p><p>✅ <strong>Eliminated the Experiment Explosion Problem</strong>: Systematic tracking of data ingestion and exploratory analysis with complete metadata</p><p>✅ <strong>Solved Artifact Management</strong>: Organized storage of datasets, visualizations, and analysis results in Azure Blob Storage</p><p>✅ <strong>Established Team Collaboration</strong>: Centralized tracking enables knowledge sharing and builds institutional memory</p><p>✅ <strong>Created Reproducible Workflows</strong>: Complete parameter logging and environment tracking for every analysis step</p><h4>Key Takeaways</h4><ol><li><strong>Centralized Tracking Transforms Data Science</strong>: Having a single source of truth for all experimental work dramatically improves team productivity</li><li><strong>Systematic Artifact Management</strong>: Organized storage of plots, data samples, and metadata creates a rich, searchable knowledge base</li><li><strong>Authentication Challenges Are Solvable</strong>: Proper Azure Service Principal setup enables seamless artifact logging from any environment</li><li><strong>Visual Analysis Preservation</strong>: EDA insights are no longer lost when notebooks are updated or overwritten</li></ol><h4>Ready for Advanced Workflows</h4><p>With this foundation in place, you’re now prepared to tackle advanced ML workflows including:</p><ul><li><strong>Pipeline Preparation</strong>: Scikit-learn preprocessing pipelines provide the foundation for robust, leak-free model training</li><li><strong>Automated Model Training</strong>: Using MLflow’s autologging capabilities for comprehensive model tracking</li><li><strong>Pipeline-Based Workflows</strong>: Combining preprocessing and model training in reproducible pipelines</li><li><strong>Multi-Model Comparison</strong>: Systematic evaluation of different algorithms and hyperparameters</li><li><strong>Deep Learning Integration</strong>: Extending tracking to TensorFlow and PyTorch workflows</li></ul><h4>What’s Next: Advanced Model Training</h4><p>In <strong>Part 4: Advanced Model Training with MLflow Pipelines and Autologging</strong>, we’ll build upon this foundation to create sophisticated training workflows that automatically capture every aspect of model development. We’ll explore:</p><ul><li>MLflow’s powerful autologging capabilities for Scikit-learn and deep learning frameworks</li><li>Building and tracking complete ML pipelines from preprocessing to model evaluation</li><li>Systematic comparison of multiple model approaches with comprehensive metrics</li><li>Integration with TensorFlow and other advanced ML frameworks</li></ul><p>The preprocessing pipeline and train/test data we’ve prepared in this article will serve as the starting point for these advanced workflows.</p><h4>Series Roadmap</h4><p>✅ <strong>Part 1</strong>: Complete ML Lifecycle Architecture Overview<br> ✅ <strong>Part 2</strong>: Production MLflow Tracking Server Setup<br> ✅ <strong>Part 3</strong>: Experiment Tracking Fundamentals (This article)<br> 🔄 <strong>Part 4</strong>: Advanced Model Training with MLflow Pipelines and Autologging (Next)<br> 📅 <strong>Part 5</strong>: Hyperparameter Tuning at Scale with MLflow<br> 📅 <strong>Part 6</strong>: Serverless Model Serving with Istio, Knative, and KServe<br> 📅 <strong>Part 7</strong>: Production Monitoring &amp; Observability<br> 📅 <strong>Part 8</strong>: Continuous Deployment with ArgoCD</p><p>Your MLflow experiment tracking foundation is now operational and ready for advanced ML workflows. In Part 4, we’ll transform this foundation into a comprehensive model training and evaluation system.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=303dc95d5d63" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Enterprise MLOps Platform on Kubernetes: Setting Up a Production-Grade MLflow Tracking Server]]></title>
            <link>https://medium.com/@wltsankalpa/enterprise-mlops-platform-on-kubernetes-setting-up-a-production-grade-mlflow-tracking-server-0ea0f49d8363?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/0ea0f49d8363</guid>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Sun, 06 Jul 2025 12:37:20 GMT</pubDate>
            <atom:updated>2025-07-19T12:52:28.646Z</atom:updated>
            <content:encoded><![CDATA[<h3>Introduction: From Architecture to Implementation</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sSc9iCwwASh862JTOSMByA.png" /></figure><p>In the <a href="https://medium.com/@wltsankalpa/enterprise-mlops-platform-on-kubernetes-complete-machine-learning-life-cycle-from-experimentation-b78a1c21e7c5">first article of this series</a>, we explored the architectural foundation of an enterprise MLOps platform on Kubernetes. We identified the critical challenges that plague ML production deployments:</p><ul><li><strong>The Experiment Explosion Problem</strong>: Teams accumulating hundreds of model variations without systematic tracking</li><li><strong>The “Golden Model” Mystery</strong>: Breakthrough results that can’t be reproduced due to lost configurations</li><li><strong>Production Integration Challenges</strong>: The skills gap between model development and robust API deployment</li><li><strong>Data Drift and Model Degradation</strong>: Silent performance decay requiring automated retraining pipelines</li></ul><p>We also established how MLflow serves as the “central nervous system” for ML operations, providing comprehensive experiment tracking, model registry, and collaboration capabilities within our broader Kubernetes-native architecture.</p><p>Now, let’s move from architecture to implementation. This article is the first in a deep-dive series that will guide you through building each component of the MLOps platform. We’ll start with the foundation: deploying a production-grade MLflow tracking server on Azure Kubernetes Service (AKS).</p><h4>What is MLflow?</h4><p>MLflow is an open-source platform designed to handle the entire machine learning lifecycle seamlessly. It’s extremely versatile and integrates smoothly with nearly every machine learning library or programming language. While MLflow offers several components, our primary focus today will be on MLflow Tracking.</p><h4>MLflow Tracking: Organized Experiment Management</h4><p>MLflow Tracking allows you to log:</p><ul><li><strong>Parameters</strong>: Inputs such as hyperparameters (e.g., learning rate, number of estimators) or feature engineering choices.</li><li><strong>Metrics</strong>: Evaluation outputs like accuracy, F1-score, RMSE, or even custom business-specific metrics.</li><li><strong>Artifacts</strong>: Files generated during experiments such as trained models, datasets, visualizations (e.g., confusion matrices, EDA plots), or code files.</li><li><strong>Models</strong>: Pre-built deployment-ready “flavors” for various ML libraries, simplifying deployment.</li></ul><p>All this information is neatly organized into experiments and runs and visualized through a user-friendly web interface, enabling easy comparisons of different runs and clear insights into metric variations relative to parameter adjustments.</p><h3>Setting Up MLflow: From Local to a Centralized Hub</h3><p>Starting with MLflow locally is straightforward. After installation (pip install mlflow), run mlflow ui from your terminal to spin up a local tracking server. Data is stored locally in the ./mlruns directory.</p><p>However, for team collaboration, robust data persistence, and a more production-like environment, a <strong>centralized MLflow tracking server</strong> is the way to go. This allows everyone on the team to log their experiments to a single, accessible location, regardless of where they are running their code (local machine, cloud VM, dedicated training clusters, etc.).</p><p>We’ll now walk through deploying a centralized MLflow server on <strong>Azure Kubernetes Service (AKS)</strong> using Helm charts. This approach provides scalability, manageability, and integrates well with other cloud-native tools. We’ll also configure it to use Azure Blob Storage for artifact persistence and a PostgreSQL database (deployed within Kubernetes via the Helm chart) for robust metadata storage.</p><h4>Step-by-Step Guide: Deploying MLflow on Azure Kubernetes Service (AKS)</h4><p>Here’s a structured approach to setting up a centralized MLflow tracking server on AKS.</p><p><strong>Step 1: Azure Environment Setup</strong></p><p>First, we’ll set up some environment variables. This makes the commands cleaner and easier to manage. These variables will define names for our Azure resources and Kubernetes configurations.</p><pre>export RESOURCE_GROUP=&quot;tharindu-mlflow-rg&quot;<br>export LOCATION=&quot;centralindia&quot;<br>export AKS_CLUSTER_NAME=&quot;tharindu-mlflow-aks&quot;<br>export STORAGE_ACCOUNT_NAME=&quot;tharindumlflow$(openssl rand -hex 4)&quot; # Needs to be globally unique<br>export CONTAINER_NAME=&quot;artifactroot&quot;<br>export K8S_NAMESPACE=&quot;mlflow&quot;</pre><ul><li><strong>RESOURCE_GROUP</strong>: A logical container for all your Azure resources for this setup.</li><li><strong>LOCATION</strong>: The Azure region where your resources will be created.</li><li><strong>AKS_CLUSTER_NAME</strong>: The name for your Kubernetes cluster.</li><li><strong>STORAGE_ACCOUNT_NAME</strong>: A unique name for the Azure Storage Account that will store MLflow artifacts. The $(openssl rand -hex 4) part helps generate a unique suffix.</li><li><strong>CONTAINER_NAME</strong>: The name of the blob container within the storage account.</li><li><strong>K8S_NAMESPACE</strong>: The Kubernetes namespace where MLflow components will be deployed, helping to isolate them.</li></ul><p>Next, log in to your Azure account. The — use-device-code option is helpful in environments without a direct browser. Then log in to Azure:</p><pre>az login --use-device-code</pre><p>Create an Azure resource group and set up an AKS cluster:</p><pre>az group create --name $RESOURCE_GROUP --location $LOCATION<br><br>az provider register --namespace Microsoft.ContainerService<br>az provider show --namespace Microsoft.ContainerService --query &quot;registrationState&quot;<br><br>az aks create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $AKS_CLUSTER_NAME \<br>  --node-count 1 \<br>  --enable-managed-identity \<br>  --generate-ssh-keys \<br>  --location $LOCATION<br><br>az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_CLUSTER_NAME --overwrite-existing</pre><ul><li>— node-count 1: For this demo, one node is sufficient. You can scale this based on expected load.</li><li>— enable-managed-identity: Allows the AKS cluster to authenticate to other Azure services (like our storage account later, though for direct key access as in this Helm chart, it’s less critical but good practice for other interactions).</li></ul><p>Once the cluster is provisioned (this might take a few minutes), get the Kubernetes configuration context so kubectl can connect to your new AKS cluster. The — overwrite-existing flag will update your local kubeconfig if a context with the same name already exists.</p><p><strong>Step 2: Configuring Azure Storage for Artifacts</strong></p><p>MLflow needs a place to store “artifacts.” These are any files generated by your experiments, such as trained model files, images (like plots), data files, or even environment configuration files. For a robust and scalable setup, using cloud storage like Azure Blob Storage is highly recommended over in-cluster storage. Next, create an Azure Storage account and container to store MLflow artifacts:</p><pre>az storage account create \<br>  --name $STORAGE_ACCOUNT_NAME \<br>  --resource-group $RESOURCE_GROUP \<br>  --location $LOCATION \<br>  --sku Standard_LRS \<br>  --kind StorageV2<br><br># Inside this storage account, create a blob container where MLflow will store the artifacts:<br>az storage container create \<br>  --name $CONTAINER_NAME \<br>  --account-name $STORAGE_ACCOUNT_NAME</pre><p>The MLflow Helm chart will need an access key to write to this storage account. Retrieve one of the account keys and store it in an environment variable. <strong>Important:</strong> Treat this key like a password; it grants full access to your storage account.</p><pre>export AZURE_STORAGE_ACCESS_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP --account-name $STORAGE_ACCOUNT_NAME --query &quot;[0].value&quot; -o tsv)</pre><p>Before deploying MLflow, it’s essential to verify the setup:</p><p><strong>Step 3: Azure Portal Verification:</strong></p><ul><li>Go to your Azure portal.</li><li>Navigate to the resource group you created earlier (tharindu-mlflow-rg).</li><li>Inside this resource group, confirm that both your AKS cluster and Azure Storage account appear.</li><li>Enter your Azure Storage account and select “Data storage.” Verify the presence of the artifactrootcontainer.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uOzZlP-kTtfVyT6eReHroQ.png" /><figcaption><strong>Azure Portal Verification</strong></figcaption></figure><p><strong>Step 4: MLflow Helm Chart Installation</strong></p><p>Helm is a package manager for Kubernetes. It simplifies the deployment and management of applications like MLflow using “charts,” which are pre-packaged sets of resource definitions.</p><p>First, add the community-charts repository, which hosts a chart for MLflow, and update your local Helm repository information:</p><pre>helm repo add community-charts https://community-charts.github.io/helm-charts<br>helm repo update</pre><p>Now, we’ll configure our MLflow installation using a <strong>mlflow-values.yaml</strong> file. This file overrides the default settings in the Helm chart. Create a file named mlflow-values.yaml with the following content:</p><pre># mlflow-values.yaml<br><br># --- PostgreSQL Sub-chart Configuration ---<br># By default, the MLflow chart might use a simple SQLite backend, which is not suitable for<br># concurrent access or production workloads. We enable the deployment of a PostgreSQL<br># database instance within our Kubernetes cluster via a sub-chart.<br># This database will store MLflow experiment metadata (parameters, metrics, run info).<br>postgresql:<br>  enabled: true  # Set to true to deploy PostgreSQL using the Bitnami PostgreSQL sub-chart.<br>  auth:<br>    # These are credentials FOR the PostgreSQL database instance itself.<br>    # MLflow server will use these to connect to the database.<br>    database: &quot;mlflow_db&quot;                 # Name of the database to be created/used within PostgreSQL.<br>    username: &quot;mlflow_user&quot;               # Username for MLflow to access the database.<br>    password: &quot;&lt;YOUR_POSTGRESQL_PASSWORD&gt;&quot; # &lt;&lt;&lt; REPLACE THIS with a strong, unique password.<br><br># --- Artifact Store Configuration ---<br># This section configures MLflow to use Azure Blob Storage as its artifact root.<br># All artifacts (models, plots, data files) will be stored here.<br>artifactRoot:<br>  azureBlob:<br>    enabled: true # Explicitly enable Azure Blob storage for artifacts.<br>    container: &quot;&lt;YOUR_AZURE_CONTAINER_NAME&gt;&quot;             # &lt;&lt;&lt; REPLACE THIS placeholder<br>    storageAccount: &quot;&lt;YOUR_AZURE_STORAGE_ACCOUNT_NAME&gt;&quot;  # &lt;&lt;&lt; REPLACE THIS placeholder<br>    accessKey: &quot;&lt;YOUR_AZURE_STORAGE_ACCESS_KEY&gt;&quot;      # &lt;&lt;&lt; REPLACE THIS placeholder<br>    # path: &quot;&quot; # Optional: If you want artifacts stored in a specific subfolder within the container.<br><br># --- Service Configuration ---<br># This defines how the MLflow service is exposed within and outside the Kubernetes cluster.<br>service:<br>  type: LoadBalancer # Exposes the MLflow UI service externally using a cloud provider&#39;s load balancer.<br>  port: 80           # The port the LoadBalancer will listen on.<br><br># --- MLflow UI Authentication ---<br># This enables basic authentication for accessing the MLflow UI.<br>auth:<br>  enabled: true # Set to true to protect your MLflow UI with a username and password.<br>  adminUsername: &quot;admin&quot;<br>  adminPassword: &quot;&lt;YOUR_MLFLOW_UI_ADMIN_PASSWORD&gt;&quot; # &lt;&lt;&lt; REPLACE THIS with a strong, unique password (longer than 12 chars).</pre><p><strong>Explaining the mlflow-values.yaml Configuration:</strong></p><p><strong>postgresql section:</strong></p><ul><li>enabled: true: We’re telling Helm to deploy a PostgreSQL database using its sub-chart. This is crucial for a multi-user or production MLflow setup because the default SQLite backend doesn’t handle concurrent writes well.</li><li>auth: Defines the credentials for the PostgreSQL database itself. MLflow will use these to store and retrieve experiment metadata (parameters, metrics, run information, etc.). <strong>It is critical to change the default password to something strong and unique.</strong></li></ul><p><strong>artifactRoot.azureBlob section:</strong></p><ul><li>enabled: true: Specifies that we want to use Azure Blob Storage for our artifacts.</li><li>container, storageAccount, accessKey: These are placeholders that we will populate with the actual values from our Azure Storage setup. Storing artifacts in Azure Blob Storage ensures they are persistent, scalable, and decoupled from the Kubernetes cluster’s lifecycle.</li></ul><p><strong>service section:</strong></p><ul><li>type: LoadBalancer: This tells Kubernetes to provision an external load balancer (provided by Azure in this case) for the MLflow service. This will give us an external IP address to access the MLflow UI from anywhere.</li><li>port: 80: The MLflow UI will be accessible on port 80 of the external IP. The service will route traffic from this port to the MLflow container’s internal port (usually 5000).</li></ul><p><strong>auth section:</strong></p><ul><li>enabled: true: This enables basic HTTP authentication for the MLflow UI itself.</li><li>adminUsername, adminPassword: These are the credentials you’ll use to log in to the MLflow web interface. <strong>Again, it is critical to change the default adminPassword to something strong and unique.</strong></li></ul><p>Now, we need to update the mlflow-values.yaml file with your specific configurations. This includes Azure Storage details for artifacts, a password for the internal PostgreSQL database, and an admin password for the MLflow UI.</p><p>Before editing the file, decide on the following:</p><ol><li><strong>PostgreSQL Password:</strong> Choose a strong, unique password for the PostgreSQL database that MLflow will use. Example: P0stgr3s_S3cur3_P@ssw0rd!.</li><li><strong>MLflow UI Admin Password:</strong> Choose a strong, unique password for the admin user to access the MLflow web UI. <strong>This password MUST be longer than 12 characters.</strong> Example: MyMLflowUI_AdminP@ssw0rd123!.</li></ol><p>Run the following commands in your terminal to display the Azure Storage values. Copy each of these carefully.</p><pre>echo &quot;Your Container Name: $CONTAINER_NAME&quot;<br>echo &quot;Your Storage Account Name: $STORAGE_ACCOUNT_NAME&quot;<br>echo &quot;Your Azure Storage Access Key: $AZURE_STORAGE_ACCESS_KEY&quot;</pre><p>Open the mlflow-values.yaml file (with the content provided above) in your preferred text editor. You will need to replace several placeholder strings:</p><p><strong>In the postgresql.auth section:</strong></p><ul><li>Replace &lt;YOUR_POSTGRESQL_PASSWORD&gt; with the strong PostgreSQL password you chose.</li></ul><p><strong>In the artifactRoot.azureBlob section:</strong></p><ul><li>Replace &lt;YOUR_AZURE_CONTAINER_NAME&gt; with the actual container name from your Azure environment (e.g., artifactroot).</li><li>Replace &lt;YOUR_AZURE_STORAGE_ACCOUNT_NAME&gt; with the actual storage account name from your Azure environment (e.g., tharindumlflowabcd).</li><li>Replace &lt;YOUR_AZURE_STORAGE_ACCESS_KEY&gt; with the actual Azure Storage access key you retrieved.</li></ul><p><strong>In the auth section (for MLflow UI):</strong></p><ul><li>Replace &lt;YOUR_MLFLOW_UI_ADMIN_PASSWORD&gt; with the strong MLflow UI admin password you chose (must be &gt;12 characters).</li></ul><p><strong>Save the mlflow-values.yaml file after making all these changes. </strong>Finally, install MLflow using Helm with your custom configurations:</p><pre>helm install mlflow community-charts/mlflow \<br>  --namespace $K8S_NAMESPACE \<br>  --create-namespace \<br>  -f mlflow-values.yaml</pre><ul><li>helm install mlflow …: Deploys a new release named mlflow using the community-charts/mlflow chart.</li><li>— namespace $K8S_NAMESPACE: Installs MLflow into the specified Kubernetes namespace.</li><li>— create-namespace: Creates the namespace if it doesn’t already exist.</li><li>-f mlflow-values.yaml: Applies the configurations from your custom values file.</li></ul><p>This command will start deploying MLflow and its PostgreSQL backend to your AKS cluster. After a few minutes, you should verify that MLflow has been deployed correctly.</p><p><strong>Step 6: Checking MLflow Status with kubectl:</strong></p><p>Run the following kubectl commands from your terminal to verify MLflow deployment:</p><pre>kubectl get pods -n $K8S_NAMESPACE<br>kubectl get svc -n $K8S_NAMESPACE<br>kubectl get pvc  -n $K8S_NAMESPACE<br>kubectl get secret  -n $K8S_NAMESPACE<br>kubectl describe deployment mlflow -n $K8S_NAMESPACE</pre><p>You should see pods for MLflow (e.g., mlflow-xxxxxxxxxx-xxxxx) and PostgreSQL (e.g., mlflow-postgresql-0) in a Running state. It might take a few moments for them to start and become ready.</p><p>Look for the mlflow service (or similar, might be mlflow-mlflow-tracking-server). It should have an EXTERNAL-IP. This IP is how you’ll access the MLflow UI. It might take a minute or two for Azure to assign an external IP. If it shows &lt;pending&gt;, wait a bit and try again.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bDJfvuTDjgy11egB2UMNnw.png" /><figcaption><strong>Checking MLflow Status with kubectl</strong></figcaption></figure><p>Once the mlflow service has an EXTERNAL-IP, you can access the MLflow UI by navigating to <a href="http://135.235.251.124/">http://135.235.251.124</a> (since we configured the service port to 80) in your web browser. You should be prompted for the username and password you set in mlflow-values.yaml.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cIif9xh_QfcM2ERan0t16w.png" /><figcaption>Prompted for the username and password</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hy7he_oKdEmUJCz41hq8qw.png" /><figcaption>Web UI</figcaption></figure><h4>Understanding Centralized MLflow Architecture</h4><p>Before we dive into using this MLflow setup from our Python notebooks, let’s take a moment to visualize the architecture we’ve just deployed. This will help clarify how the different components interact.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pRXX6DCy0940L6R6K2soaA.png" /><figcaption><em>Architecture of the centralized MLflow deployment on Azure Kubernetes Service</em></figcaption></figure><p><strong>Key Components and Flow:</strong></p><p><strong>Azure Kubernetes Service (AKS):</strong> This is our managed Kubernetes environment where the MLflow application runs.</p><ul><li><strong>MLflow Namespace (within AKS):</strong> We’ve deployed all MLflow-related Kubernetes resources (pods, services, PVCs) into a dedicated namespace (e.g., mlflow) for better organization and management.</li><li><strong>MLflow Server (Pod within MLflow Namespace):</strong> This pod runs the main MLflow application (the Python server). It handles API requests for logging experiments, querying runs, and serving the web UI.</li><li><strong>PostgreSQL Database (Pod within MLflow Namespace):</strong> Deployed as another pod within the Kubernetes cluster (managed by the Helm sub-chart). This database serves as the <strong>Backend Store</strong>. It stores all the metadata associated with your experiments: parameters, metrics, run IDs, user information, model registry details, etc.</li><li><strong>PersistentVolumeClaim (PVC: data-mlflow-postgresql-0):</strong> The PostgreSQL database uses a PVC to request persistent storage from Azure (typically Azure Disk). This ensures that your metadata isn’t lost if the PostgreSQL pod restarts.</li><li><strong>LoadBalancer Service (within MLflow Namespace):</strong> This Kubernetes service of type LoadBalancer exposes the MLflow Server to the internet via a public IP address and port (e.g., port 80). This is the entry point for both users accessing the UI and for MLflow clients sending tracking data.</li></ul><p><strong>Azure Blob Storage (artifactroot container):</strong> This is our <strong>Artifact Store</strong>, configured to use a specific container (e.g., artifactroot). The MLflow Server stores and retrieves larger files (artifacts) here, such as:</p><ul><li>Trained model files (e.g., pickled Scikit-learn models, TensorFlow SavedModels).</li><li>Data files (e.g., processed datasets).</li><li>Images (e.g., plots generated during EDA or model evaluation like confusion matrices).</li><li>Environment files (e.g., conda.yaml).<br>Using external blob storage is crucial for scalability and decoupling artifacts from the Kubernetes cluster’s lifecycle.</li></ul><p><strong>Users &amp; Development Environments:</strong></p><ul><li><strong>Data Scientists / ML Engineers:</strong> These are the primary users of the MLflow system.</li><li>They interact with the MLflow Server via its <strong>LoadBalancer Service</strong>.</li><li>They use the MLflow UI to view experiments, compare runs, and manage models in the Model Registry.</li><li>From their development environments, they use the MLflow client libraries to log experiments.</li><li><strong>Local Notebooks / Cloud Notebooks:</strong> This could be:</li><li>Jupyter Notebooks running on a data scientist’s laptop.</li><li>Cloud-based Notebooks such as Azure Machine Learning notebooks, Databricks, Google Colab, or any other managed notebook environment.</li><li>In both cases, the Python code in these notebooks will be configured with the <strong>Tracking URI</strong> (the address of our MLflow server’s LoadBalancer) and credentials to send data to the centralized server.</li></ul><p>This setup provides a robust, scalable, and collaborative platform for managing the machine learning lifecycle. With the metadata in PostgreSQL and artifacts in Azure Blob Storage, your experiment tracking is persistent and reliable.</p><h3>Conclusion: Your MLflow Foundation is Ready</h3><h4>What We’ve Accomplished</h4><p>In this article, we’ve successfully deployed a production-grade MLflow tracking server that addresses several key challenges identified in Part 1:</p><ul><li><strong>Centralized Experiment Tracking</strong>: No more scattered notebooks or lost experiments. Every team member can now log to a single source of truth.</li><li><strong>Scalable Architecture</strong>: Our Kubernetes-based deployment can grow with your team’s needs.</li><li><strong>Enterprise Security</strong>: Proper authentication for the UI.</li><li><strong>Cloud-Native Integration</strong>: Leveraging Azure’s managed services for reliability and reduced operational overhead.</li></ul><h4>Key Takeaways</h4><ol><li><strong>Infrastructure as Code</strong>: Using Helm charts for MLflow deployment ensures reproducibility and version control of your ML infrastructure.</li></ol><h4>What’s Next: From Infrastructure to Insights</h4><p>With our MLflow tracking server operational, we’re ready to tackle the next challenge: actually using it for experiment tracking. In the next article, <strong>“Experiment Tracking Fundamentals with MLflow”</strong>, we’ll explore:</p><ul><li><strong>Connecting Your Development Environment</strong>: Setting up Python environments to communicate with our centralized server</li><li><strong>Tracking Your First Experiments</strong>: Moving beyond print statements to comprehensive experiment logging</li><li><strong>Security First</strong>: The AuthorizationPermissionMismatch troubleshooting wasn&#39;t just about fixing an error—it highlighted the importance of understanding Azure&#39;s identity and access management for ML workflows.</li><li><strong>Managing Artifacts at Scale</strong>: Best practices for organizing models, plots, and datasets</li><li><strong>Collaborative Workflows</strong>: How teams can share insights and build on each other’s work</li></ul><p>We’ll use the UCI Adult Income dataset as our example, demonstrating how proper experiment tracking transforms a typical data science workflow from chaos to clarity.</p><h4>Before You Continue</h4><p>Before moving to the next article, ensure you:</p><ul><li>Can access your MLflow UI at http://&lt;YOUR-EXTERNAL-IP&gt;</li><li>Understand the architecture of your deployed system</li></ul><h3>Series Roadmap</h3><p>Here’s where we’re headed in this series:</p><ol><li>✅ <strong>Part 1</strong>: Enterprise MLOps Platform on Kubernetes: Complete Machine Learning Life Cycle from Experimentation to Deployment <em>(Completed)</em></li><li>✅ <strong>Part 2</strong>: Enterprise MLOps Platform on Kubernetes: Setting Up a Production-Grade MLflow Tracking Server on Kubernetes <em>(This article)</em></li><li>🔄 <strong>Part 3</strong>: Enterprise MLOps Platform on Kubernetes: Experiment Tracking Fundamentals with MLflow <em>(Next)</em></li><li>📅 <strong>Part 4</strong>: Enterprise MLOps Platform on Kubernetes: Advanced Model Training with MLflow Pipelines and Autologging</li><li>📅 <strong>Part 5</strong>: Enterprise MLOps Platform on Kubernetes: Hyperparameter Tuning at Scale with MLflow</li><li>📅 <strong>Part 6</strong>: Enterprise MLOps Platform on Kubernetes: Serverless ML Model Serving with Istio, Knative, and KServe</li><li>📅 <strong>Part 7</strong>: Enterprise MLOps Platform on Kubernetes: Production Monitoring &amp; Observability</li><li>📅 <strong>Part 8</strong>: Enterprise MLOps Platform on Kubernetes: Continuous Deployment/Delivery with ArgoCD</li></ol><p>Ready to start tracking your ML experiments? Let’s continue to <a href="https://medium.com/link-to-article-3">Part 3: Experiment Tracking Fundamentals with MLflow</a>.</p><p><em>Found this article helpful? Follow me for more insights on building production ML systems, and don’t forget to share your deployment experiences in the comments!</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0ea0f49d8363" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Enterprise MLOps Platform on Kubernetes: Complete Machine Learning Life Cycle from Experimentation…]]></title>
            <link>https://medium.com/@wltsankalpa/enterprise-mlops-platform-on-kubernetes-complete-machine-learning-life-cycle-from-experimentation-b78a1c21e7c5?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/b78a1c21e7c5</guid>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Sun, 22 Jun 2025 15:34:56 GMT</pubDate>
            <atom:updated>2025-06-22T15:34:56.603Z</atom:updated>
            <content:encoded><![CDATA[<h3>Enterprise MLOps Platform on Kubernetes: Complete Machine Learning Life Cycle from Experimentation to Deployment</h3><h3>Introduction</h3><blockquote>Understanding MLOps and Why It Matters</blockquote><p>Machine Learning Operations (MLOps) extends traditional DevOps practices to address the unique challenges of machine learning systems. Unlike conventional software, where deployments remain relatively stable and predictable after deployment, ML systems operate in a dynamic environment where:</p><ul><li><strong>Data continuously evolves</strong>, requiring constant adaptation</li><li><strong>Models degrade over time</strong> as real-world patterns change</li><li><strong>Performance requirements shift</strong> with changing business needs’</li></ul><p>MLOps bridges three critical disciplines: machine learning research, software engineering, and data engineering. This convergence creates a unified approach for deploying, monitoring, and maintaining ML systems at enterprise scale.</p><figure><img alt="Conventional Software Engineering Operations vs. Machine Learning Engineering Operations" src="https://cdn-images-1.medium.com/max/1024/1*anuiDeNHDjJfZ41nMjRW9g.png" /></figure><blockquote>“A machine learning system includes many of the traditional components and considerations of DevOps in a conventional software system. However, it also introduces a unique set of requirements and components specific to machine learning and AI, as shown on the left. Therefore, when developing ML and AI systems, it’s essential to adopt a new approach to address these considerations. Otherwise, most ML and AI initiatives may fail to deliver the expected business value and never reach production.”</blockquote><p><a href="https://venturebeat.com/ai/why-do-87-of-data-science-projects-never-make-it-into-production/">Why do 87% of data science projects never make it into production?</a></p><blockquote>Why Most ML Projects Fail in Production?</blockquote><p>Let’s examine the systemic challenges that plague real-world ML implementations:</p><h3>The Hidden Complexity of Production Machine Learning</h3><blockquote>“When considering a production-grade machine learning workflow or lifecycle, transitioning from a successful Jupyter notebook experiment to a production ML system presents a unique set of challenges that don’t exist in traditional software development and aren’t addressed by standard DevOps practices.”</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HrUaLKmLmY6pnfX9xwcZZQ.png" /><figcaption>Production grade ML workflow</figcaption></figure><h3>1. Experimental Nature vs. Production Stability</h3><p>Data science is inherently experimental. Data scientists continuously iterate through different algorithms, feature engineering approaches, and hyperparameter configurations. However, this experimental mindset conflicts with the stability and predictability required in production systems.</p><h4>The Experiment Explosion Problem</h4><p>Data scientists naturally iterate rapidly — testing different preprocessing pipelines, comparing algorithms, and fine-tuning hyperparameters across dozens of configurations. While scientifically sound, this experimental approach quickly becomes unmanageable.</p><p>Within weeks, teams accumulate hundreds of model variations without systematic tracking, resulting in analysis paralysis and loss of institutional knowledge about what actually works.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q50wSHJqHDzKQ0nxw_idvA.png" /></figure><blockquote>“During the machine learning experimentation and research cycle for a specific use case, data scientists try various algorithms, data preprocessing steps, feature sets, and hyperparameter configurations. These experiments are often conducted in an ad-hoc manner using Jupyter notebooks. Over time, the number of experiments and model variations can grow exponentially, making it increasingly difficult to manage and track which experiments were successful and which were not. Without a proper centralized tracking solution for ML experimentation, different experiments can become scattered across team members, notebooks, repositories, and codebases. Additionally, during experimentation, some notebooks or files may be overwritten, making them unrecoverable and impossible to reproduce.”</blockquote><h4><strong>The “Golden Model” Mystery</strong></h4><p><strong>Picture this scenario:</strong> Your team achieves breakthrough results on Friday afternoon. Monday morning arrives, and nobody can reproduce those results. The exact combination of data preprocessing, feature engineering, and model configuration that led to success has vanished — overwritten, lost, or scattered across notebooks.</p><p>This isn’t just frustrating — it’s a business risk when promising models can’t be reliably reproduced or deployed.</p><h4>Reproducibility Crisis</h4><p>A model that performs exceptionally well in a data scientist’s local environment may fail to reproduce the same results in production due to differences in:</p><ul><li>Data preprocessing steps</li><li>Library versions</li><li>Computational environments</li><li>Random seed initialization</li></ul><h4>Collaboration Complexity</h4><p>Machine learning projects involve diverse stakeholders — data scientists, ML engineers, data engineers, and DevOps teams — each with different tools, workflows, and success metrics. Without proper coordination mechanisms, these teams often work in silos, leading to integration challenges.</p><p>Most organizations struggle with:</p><ul><li>Experiment details scattered in individual notebooks</li><li>Model configurations shared through email or Slack</li><li>Inconsistent environments between development and production</li><li>Lack of visibility into what models are actually running in production</li></ul><h3>2. The Production Integration Challenge</h3><p>Most end-users interact with enterprise-grade software, such as web or mobile applications. To serve model outputs — such as inferences or predictions — to these users, the machine learning model must be successfully integrated with the user-facing system. End-users never interact directly with an ML model or a Jupyter notebook; they connect through the enterprise-grade application. The most practical way to integrate ML models into enterprise applications is through RESTful APIs.</p><h4>Building Production-Ready Model APIs</h4><p>While data scientists excel at model development, they’re rarely backend engineers, creating an immediate skills gap. When it comes to creating robust model-serving APIs, the process closely resembles traditional software engineering. This is because it involves applying best practices and addressing the typical engineering challenges associated with API development, especially in the context of building reliable software systems. A robust model serving API must address:</p><p><strong>Operational Excellence:</strong></p><ul><li>High availability and fault tolerance under varying load conditions</li><li>Horizontal scaling capabilities to handle traffic spikes</li><li>Graceful degradation when dependencies fail</li></ul><p><strong>Security and Governance:</strong></p><ul><li>Authentication and authorization mechanisms</li><li>Request/response logging for audit trails</li><li>Protection against adversarial inputs</li></ul><p><strong>Advanced Deployment Patterns:</strong></p><ul><li>Canary releases for safe model updates</li><li>A/B testing frameworks for model comparison</li><li>Blue-green deployments for zero-downtime updates</li></ul><h4>Model serving Infrastructure</h4><p>Another major challenge is model serving infrastructure. Hosting machine learning models for inference can be costly in terms of hardware resources. In scenarios where each customer has their own database and trains the same model on their specific data, this can lead to hundreds or even thousands of models. Even for the same use case, different customers may require separate models due to variations in their data. Hosting these models on dedicated infrastructure for each customer can be highly challenging, particularly in ensuring proper isolation and meeting data privacy compliance requirements.</p><p>Moreover, many of these models may receive inference requests infrequently — perhaps only once a day or even once a week. As a result, expensive infrastructure like GPUs and high-memory CPUs may remain idle most of the time. This inefficiency highlights the need for infrastructure optimization.</p><p>To address this, serverless model serving becomes essential for production-grade machine learning systems. Serverless solutions should be able to automatically scale resources up in response to inference requests and scale down to zero when idle, optimizing both cost and performance, and still ensuring the proper isolation and meeting data privacy compliance requirements.</p><h3>3. Data Dependency and Drift</h3><p>ML models fundamentally depend on training data patterns. As real-world data distributions shift over time, model performance can silently degrade — a phenomenon known as “data drift” or “concept drift.”</p><p>In scenarios where data drift occurs — meaning the underlying data distribution and patterns change — the performance of machine learning models can degrade, even in the absence of any code-level issues. This can cause ML systems to fail unexpectedly. To address this, models need to be retrained. However, manually retraining models in a Jupyter Notebook environment is not acceptable for reliable, enterprise-grade applications.</p><p>An automated retraining process is essential. This process should be triggered when model performance drops below a defined threshold. The automated model retraining pipeline should encompass all necessary steps, including:</p><ul><li>Automated data collection</li><li>Data quality validation</li><li>Model retraining, potentially with support for transfer learning</li><li>Automated model validation</li><li>Decision-making for deployment, either auto-deploy or requiring manual review</li></ul><p>Furthermore, the pipeline must also account for concept drift. In such cases, even retraining the same model on new data may not yield acceptable results. These situations require manual intervention, and data scientists should be alerted for review and potential model redesign.</p><p>This level of automation and monitoring ensures the reliability and adaptability of ML systems in dynamic production environments.</p><h4>Monitoring and Maintenance Requirements</h4><p>Production ML systems require continuous monitoring across multiple dimensions:</p><ul><li><strong>Performance Monitoring:</strong> Track inference latency, throughput, and resource utilization to ensure SLA compliance</li><li><strong>Model Quality Monitoring:</strong> Detect statistical drift in input data and model performance degradation over time</li><li><strong>Automated Response Systems:</strong> Implement continuous training pipelines that automatically retrain models when performance thresholds are breached</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eTzncQIiXrkYHN1HdHe5Rg.png" /></figure><h3>4. Technical Debt Accumulation</h3><p>The rapid prototyping nature of ML development can quickly accumulate technical debt. Models built with quick fixes and experimental code paths become difficult to maintain, update, or scale when moved to production environments.</p><h3>Solution: Infrastructure as Code for ML</h3><p>The fundamental issue underlying these challenges is the lack of structured, repeatable processes for ML lifecycle management. Traditional development environments like Jupyter notebooks, while excellent for experimentation, provide no native support for:</p><ul><li>Systematic experiment tracking and comparison</li><li>Reproducible environment management</li><li>Automated testing and validation pipelines</li><li>Seamless production deployment workflows</li></ul><p><strong>Solution Philosophy:</strong> Create a unified platform that empowers data scientists to manage the complete ML lifecycle — from initial experimentation to production monitoring — without requiring deep infrastructure expertise.</p><h3>Technology Stack: Strategic Architecture Decisions</h3><p>Building an enterprise-grade MLOps platform requires careful selection of technologies that work together seamlessly. Here’s my recommended technology stack and the strategic reasoning behind each choice:</p><h3>Core ML Lifecycle Management</h3><p><strong>MLflow</strong> serves as the central nervous system for ML operations, providing:</p><ul><li>Comprehensive experiment tracking with automatic logging</li><li>Centralized model registry with versioning and stage management</li><li>Reproducible model packaging and deployment artifacts</li><li>Team collaboration through shared experiment visibility</li></ul><p><a href="https://mlflow.org/docs/latest/ml/">MLflow for ML Models | MLflow AI Platform | MLflow</a></p><h3>Service Mesh and Traffic Management</h3><p><strong>Istio</strong> creates a sophisticated service mesh that handles:</p><ul><li>Advanced traffic routing and load balancing</li><li>Mutual TLS for secure service-to-service communication</li><li>Comprehensive observability with distributed tracing</li><li>Policy enforcement and security controls</li></ul><p>This foundation enables complex deployment patterns like canary releases and A/B testing without application-level changes.</p><p><a href="https://istio.io/latest/docs/overview/what-is-istio/">What is Istio?</a></p><h3>Serverless Model Serving</h3><p><strong>Knative</strong> adds serverless capabilities to our Kubernetes platform:</p><ul><li>Automatic scaling based on request volume (including scale-to-zero)</li><li>Efficient resource utilization through dynamic provisioning</li><li>Built-in revision management for deployment strategies</li></ul><p><a href="https://knative.dev/docs/">Home - Knative</a></p><p><strong>KServe</strong> provides standardized model serving with enterprise features:</p><ul><li>Protocol standardization across different ML frameworks</li><li>Built-in support for explainability and bias detection</li><li>Advanced autoscaling policies</li><li>Native integration with monitoring systems</li></ul><p><a href="https://kserve.github.io/website/0.10/">Home - KServe Documentation Website</a></p><h3>Observability and Monitoring</h3><p><strong>Prometheus &amp; Grafana</strong> create a comprehensive monitoring ecosystem:</p><ul><li>Real-time metrics collection from all system components</li><li>Custom ML-specific metrics (model accuracy, drift detection)</li><li>Alerting systems for proactive issue resolution</li><li>Rich visualization dashboards for operational insights</li></ul><ul><li><a href="https://prometheus.io/docs/introduction/overview/">Overview | Prometheus</a></li><li><a href="https://grafana.com/">Grafana: The open and composable observability platform | Grafana Labs</a></li></ul><h3>GitOps and Deployment Automation</h3><p><strong>ArgoCD</strong> implements GitOps principles for infrastructure management:</p><ul><li>Declarative configuration management through Git</li><li>Automated synchronization between desired and actual state</li><li>Audit trails for all infrastructure changes</li><li>Rollback capabilities for failed deployments</li></ul><p><a href="https://argo-cd.readthedocs.io/en/stable/">Argo CD - Declarative GitOps CD for Kubernetes</a></p><p><strong>Argo Workflows</strong> orchestrates complex ML pipelines:</p><ul><li>Automated model retraining triggered by performance degradation</li><li>Data drift detection and response workflows</li><li>Parallel execution of training experiments</li><li>Integration with external data sources and systems</li></ul><p><a href="https://argo-workflows.readthedocs.io/en/latest/">Argo Workflows - The workflow engine for Kubernetes</a></p><h3>Development and Integration Workflows</h3><p><strong>GitHub Actions</strong> provides CI/CD automation for ML workflows:</p><ul><li>Automated testing of model training pipelines</li><li>Container image building and security scanning</li><li>Integration testing for model APIs</li><li>Automated deployment to staging environments</li></ul><p><a href="https://github.com/features/actions">GitHub Actions · CI/CD and automations · Agentic Workflows · Workflows</a></p><p><strong>Helm Charts</strong> simplifies complex deployments through:</p><ul><li>Templated Kubernetes resource management</li><li>Environment-specific configuration management</li><li>Dependency management between services</li><li>Reproducible deployment processes</li></ul><p><a href="https://helm.sh/docs/">Docs Home</a></p><h3>Architecture Overview: A Multi-Layered Approach</h3><p>The platform architecture is organized into several interconnected layers, each addressing specific MLOps challenges while maintaining seamless integration with other components.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*skiU3estARIG37s-9Zsp_Q.png" /></figure><h3>Development and Experimentation Layer</h3><p>The foundation of our MLOps platform supports data scientists across various computational contexts:</p><p><strong>Local Development:</strong> Data scientists begin in familiar environments — local Jupyter notebooks on their development machines. This maintains the experimental freedom crucial for ML research while preparing for seamless integration with the centralized platform.</p><p><strong>Cloud-Based Experimentation:</strong> For intensive computational requirements, the platform supports cloud notebooks through services like Azure Databricks, Google Colab, and SageMaker Studio. This hybrid approach ensures teams can scale computational resources as needed while maintaining consistent integration patterns.</p><p>The critical innovation is <strong>unified tracking integration</strong>. Regardless of where experiments run — locally, in the cloud, or on shared compute clusters — all experimental data flows into the centralized MLflow tracking server through standardized SDK calls.</p><h3>Centralized ML Lifecycle Management</h3><p><strong>MLflow Server</strong> serves as the heart of our MLOps platform, deployed as a highly available service within our Azure Kubernetes Service (AKS) cluster. This centralized deployment provides several enterprise-critical capabilities:</p><p><strong>Experiment Tracking at Scale:</strong> Every model training run, regardless of its origin, automatically logs parameters, metrics, and artifacts to the central MLflow server. This creates an organizational knowledge base that prevents “lost experiments” and enables sophisticated experiment comparison and analysis.</p><p><strong>Model Registry and Governance:</strong> MLflow’s model registry provides a centralized catalog of all organizational models. Models progress through defined stages (Staging, Production, Archived) with proper approval workflows and access controls, creating the governance structure necessary for enterprise ML deployment.</p><p><strong>Artifact Management Integration:</strong> The platform integrates deeply with Azure Blob Storage for artifact persistence. Large model files, training datasets, and experiment outputs are stored in cloud storage while maintaining seamless access through MLflow’s artifact API, providing virtually unlimited storage capacity while maintaining cost efficiency.</p><h3>Kubernetes-Native Infrastructure Foundation</h3><p>The entire platform runs on Azure Kubernetes Service (AKS), providing the scalable, cloud-native foundation required for enterprise ML workloads:</p><p><strong>PostgreSQL Metadata Store:</strong> A dedicated PostgreSQL database deployed as a StatefulSet stores all MLflow metadata — experiment parameters, metrics, model versioning information, and user access controls. This database is configured with persistent storage and automated backup policies to ensure data durability.</p><p><strong>Istio Service Mesh:</strong> Istio creates a sophisticated service mesh handling cross-cutting concerns across the entire platform:</p><ul><li><strong>Traffic Management:</strong> Intelligent load balancing and routing for all service-to-service communication</li><li><strong>Security:</strong> Mutual TLS encryption for all internal communications with sophisticated policy enforcement</li><li><strong>Observability:</strong> Comprehensive metrics collection, distributed tracing, and service dependency mapping</li><li><strong>Advanced Deployment Patterns:</strong> Support for canary deployments, A/B testing, and blue-green deployments without application code changes</li></ul><h3>Serverless Model Serving with KServe</h3><p>The model serving layer represents one of the most sophisticated aspects of our platform. KServe, built on Knative, provides enterprise-grade model serving with minimal operational overhead.</p><h3>Understanding KServe’s Integration</h3><p>When we deploy a KServe InferenceService, a complex orchestration process demonstrates the power of cloud-native integration:</p><p><strong>Step 1: InferenceService Creation</strong></p><pre>apiVersion: serving.kserve.io/v1beta1<br>kind: InferenceService<br>metadata:<br>  name: adult-income-model<br>spec:<br>  predictor:<br>    model:<br>      modelFormat:<br>        name: mlflow<br>      storageUri: &quot;https://tharindumlflow615422a9.blob.core.windows.net/...&quot;<br>      runtime: kserve-mlflow-xgboost</pre><p><strong>Step 2: Automatic Resource Generation</strong> The KServe controller automatically generates several Kubernetes resources:</p><ul><li><strong>Knative Service:</strong> Provides serverless scaling capabilities</li><li><strong>Knative Configuration:</strong> Manages immutable revisions for rollback capabilities</li><li><strong>Knative Route:</strong> Handles traffic splitting for deployment strategies</li><li><strong>Istio VirtualService:</strong> Configures advanced routing rules</li><li><strong>Service and Deployment:</strong> Standard Kubernetes resources for the actual workload</li></ul><p><strong>Step 3: Pod Architecture</strong> Each InferenceService pod contains multiple containers working in concert:</p><ul><li><strong>Init Container:</strong> Downloads model artifacts from Azure Blob Storage using configured service account credentials</li><li><strong>Model Container:</strong> Runs our custom MLflow runtime based on MLServer, exposing the model through KServe’s standardized v2 inference protocol</li><li><strong>Knative Queue Proxy:</strong> Manages request queuing, autoscaling metrics, and graceful shutdown</li><li><strong>Istio Sidecar (Envoy Proxy):</strong> Handles traffic management, security policies, and observability data collection</li></ul><h3>Custom ClusterServingRuntime for MLflow Models</h3><p>Our platform includes a custom ClusterServingRuntime that bridges MLflow models with KServe’s serving infrastructure:</p><pre>apiVersion: serving.kserve.io/v1alpha1<br>kind: ClusterServingRuntime<br>metadata:<br>  name: kserve-mlflow-xgboost<br>spec:<br>  supportedModelFormats:<br>  - name: mlflow<br>    version: &quot;2&quot;<br>    autoSelect: true<br>  containers:<br>  - name: kserve-container<br>    image: tharindumlflowacr2f76fa.azurecr.io/mlflow-xgboost-serving:v1.0.0</pre><p>This runtime definition tells KServe how to serve MLflow models using our custom container image, which includes:</p><ul><li><strong>MLServer:</strong> Provides KServe v2 protocol compatibility</li><li><strong>MLflow Runtime:</strong> Loads and serves MLflow models natively</li><li><strong>XGBoost Dependencies:</strong> Optimized libraries for XGBoost model inference</li><li><strong>Azure Integration:</strong> Libraries for accessing Azure Blob Storage artifacts</li></ul><h3>Serverless Scaling and Resource Optimization</h3><p>The Knative integration provides sophisticated autoscaling capabilities:</p><p><strong>Scale-to-Zero:</strong> During periods of no traffic, InferenceService pods automatically scale down to zero, eliminating resource costs while maintaining the ability to rapidly scale up when requests arrive.</p><p><strong>Traffic-Based Scaling:</strong> The platform monitors request rates and automatically scales pods based on configurable metrics:</p><ul><li>Requests per second (RPS)</li><li>Concurrent request count</li><li>Custom metrics like inference latency</li></ul><p><strong>Revision Management:</strong> Each model update creates a new Knative revision, enabling:</p><ul><li>Instant rollback to previous model versions</li><li>Traffic splitting between model versions for A/B testing</li><li>Gradual rollout of new models with canary deployment patterns</li></ul><h3>GitOps and Continuous Deployment</h3><p>The platform implements GitOps principles through ArgoCD, creating a declarative approach to infrastructure and application management:</p><p><strong>App-of-Apps Pattern:</strong> A root ArgoCD application manages other applications, creating a hierarchical deployment structure that scales to complex, multi-environment scenarios.</p><p><strong>Helm-Based Templating:</strong> Both MLflow infrastructure and KServe model deployments use Helm charts, providing:</p><ul><li>Environment-specific configuration management</li><li>Dependency management between components</li><li>Standardized deployment patterns across teams</li></ul><p><strong>Git-Driven Deployments:</strong> All infrastructure changes flow through Git repositories, ensuring:</p><ul><li>Complete audit trails for all changes</li><li>Peer review processes for critical updates</li><li>Automated rollback capabilities</li><li>Configuration drift detection and remediation</li></ul><h3>Advanced Observability and Monitoring</h3><p>The platform includes comprehensive observability across all layers:</p><h3>Prometheus Metrics Collection</h3><p>Comprehensive metrics gathering from:</p><ul><li><strong>Model Containers:</strong> Custom ML metrics (accuracy, latency, throughput)</li><li><strong>Knative Components:</strong> Autoscaling metrics, request queuing statistics</li><li><strong>Istio Sidecars:</strong> Service mesh traffic patterns, error rates, latency distributions</li><li><strong>Infrastructure:</strong> Kubernetes cluster health, resource utilization</li></ul><h3>Distributed Tracing with Jaeger</h3><p>Complete request tracing from external clients through the service mesh to individual model inference calls, enabling:</p><ul><li>Performance bottleneck identification</li><li>Error propagation analysis</li><li>Service dependency mapping</li></ul><h3>Grafana Visualization</h3><p>Rich dashboards providing:</p><ul><li>Real-time model performance metrics</li><li>Infrastructure utilization trends</li><li>Business impact tracking</li><li>Alerting for anomalous conditions</li></ul><h3>Kiali Service Mesh Visualization</h3><p>Interactive topology views showing:</p><ul><li>Service communication patterns</li><li>Traffic flow volumes and error rates</li><li>Security policy effectiveness</li><li>Configuration validation</li></ul><h3>Continuous Integration and Model Lifecycle Automation</h3><h3>GitHub Actions Integration</h3><p>The CI/CD pipeline orchestrates a sophisticated workflow treating ML components as first-class software artifacts:</p><p><strong>Component Containerization:</strong> Each stage of Argo workflow (data validation, data processing, model training, hyperparameter tuning, model validation) is packaged as individual containers with dedicated applications.</p><p><strong>Testing Framework:</strong></p><ul><li><strong>Unit Testing:</strong> Automated testing of individual workflow components including data validation logic, feature engineering functions, and model training scripts</li><li><strong>Integration Testing:</strong> End-to-end pipeline testing to ensure components work together seamlessly</li><li><strong>Container Build Automation:</strong> Automated Docker image creation with proper versioning aligned to Git commits</li></ul><h3>Argo Workflows for ML Pipelines</h3><p>Sophisticated workflow orchestration enables:</p><p><strong>Multi-Source Data Ingestion:</strong> Orchestrated data collection from databases, APIs, file systems, and streaming sources with proper error handling and retry mechanisms.</p><p><strong>Data Quality Validation:</strong> Automated schema validation, completeness checks, statistical profiling, and data quality scoring before pipeline progression.</p><p><strong>Automated Retraining:</strong> Workflows triggered by model performance degradation or data drift detection.</p><p><strong>Advanced Data Preprocessing:</strong> Parallel execution of data cleaning, transformation, and feature engineering workflows with automatic lineage tracking.</p><p><strong>Model Validation:</strong> Comprehensive testing before promoting models to production.</p><h3>Security and Compliance</h3><p>The platform incorporates security best practices throughout:</p><p><strong>Service Account Management:</strong> Dedicated service accounts with minimal required permissions for each component, following the principle of least privilege.</p><p><strong>Secret Management:</strong> Kubernetes secrets for sensitive data like Azure storage keys, with integration to Azure Key Vault for enterprise secret management.</p><p><strong>Network Policies:</strong> Istio-based network segmentation ensuring components can only communicate through explicitly defined channels.</p><p><strong>Audit Logging:</strong> Comprehensive logging of all platform interactions for compliance and security monitoring.</p><h3>Platform Benefits: Beyond Traditional Model Serving</h3><p>This architecture provides several key advantages over traditional approaches:</p><p><strong>Developer Productivity:</strong> Data scientists can focus on model development while the platform handles operational complexity automatically.</p><p><strong>Operational Excellence:</strong> Built-in monitoring, scaling, and reliability features reduce the operational burden on ML engineering teams.</p><p><strong>Standardization:</strong> Consistent deployment patterns and APIs across all models, regardless of underlying ML framework.</p><p><strong>Cost Optimization:</strong> Serverless scaling eliminates waste from idle resources while maintaining performance during peak loads.</p><p><strong>Enterprise Governance:</strong> Complete audit trails, approval workflows, and access controls necessary for regulated industries.</p><p><strong>Technology Agnostic:</strong> While demonstrated on Azure, the platform can run on any Kubernetes distribution, avoiding vendor lock-in.</p><h3>Conclusion: A Foundation for ML Success</h3><p>The result is a comprehensive MLOps platform that enables organizations to move from experimental ML projects to production-grade, scalable ML services with confidence and speed. This foundation supports advanced workflows including automated model retraining, sophisticated deployment strategies, and comprehensive model lifecycle management.</p><p>By addressing the fundamental challenges of ML production deployment — reproducibility, collaboration, monitoring, and scaling — this platform creates an environment where data science teams can focus on what they do best: building innovative ML solutions that drive business value.</p><p>In the next article, we’ll dive deep into the implementation details of setting up a centralized MLflow tracking server on AKS, complete with step-by-step instructions and configuration files.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b78a1c21e7c5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Demystifying the LangChain Ecosystem for LLM-Powered Application Development]]></title>
            <link>https://medium.com/@wltsankalpa/demystifying-the-langchain-ecosystem-for-llm-powered-application-development-6c1fbff064ea?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/6c1fbff064ea</guid>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Fri, 21 Mar 2025 18:20:11 GMT</pubDate>
            <atom:updated>2025-03-21T18:20:11.545Z</atom:updated>
            <content:encoded><![CDATA[<p>This comprehensive yet highly practical guide explores the LangChain framework, an essential toolkit for developers building applications powered by large language models (LLMs). In just one hour, beginners can gain hands-on understanding and experience with LangChain through clear, executable examples.</p><p>The article covers:</p><ol><li><strong>The LangChain Ecosystem</strong> — Core libraries, integrations, LangGraph, LangSmith, and platform solutions</li><li><strong>LangChain Architectural Evolution</strong> — Understanding the shift from monolithic to modular package structure</li><li><strong>Development Approaches</strong> — When to use direct LangChain compoents, LCEL, or LangGraph</li><li><strong>Setting Up a Development Environment</strong> — Using pyenv and Poetry for Python version management</li><li><strong>Fundamental Components</strong> — LLM providers, prompts, document loaders, text splitters, and vector databases</li></ol><p>The article provides a seamless learning path where anyone interested can:</p><ol><li>Set up a complete development environment from scratch using pyenv and Poetry</li><li>Run working code examples that demonstrate core LangChain components within VS code and Jupyter notebooks</li><li>Build practical understanding of the ecosystem’s architecture and capabilities</li></ol><p>Whether you’re new to LLM application development or looking to optimize your existing workflows, this guide provides practical examples and best practices for leveraging the full potential of the LangChain ecosystem.</p><h3>What is LangChain?</h3><p>LangChain is a framework designed to simplify the development of applications powered by large language models (LLMs). It provides tools, components, and abstractions that make it easier to build complex AI applications that leverage LLMs in more sophisticated ways than simple prompt-response interactions.</p><h3>The LangChain Ecosystem</h3><p>The LangChain ecosystem consists of several key components:</p><ol><li><strong>LangChain Core Libraries</strong>:</li></ol><ul><li>langchain-core: Contains the fundamental abstractions for chat models and basic components</li><li>langchain: The main package with implementations of chains, agents, and retrieval strategies</li><li>langchain-community: Houses third-party integrations maintained by the community</li></ul><p><strong>2. Integrations</strong>: Lightweight packages connecting LangChain to specific LLM providers (like OpenAI, Anthropic, etc.) and other tools.</p><p><strong>3. LangGraph</strong>: An orchestration framework that allows you to build stateful agents and complex workflows with LLMs. It helps manage the flow of information between different components.</p><p><strong>4. LangSmith</strong>: A development and debugging platform that helps with monitoring, evaluating, and improving LLM applications. It provides visibility into what’s happening inside your application.</p><p><strong>5. LangGraph Platform</strong>: A commercial deployment solution for turning LangGraph applications into production-ready APIs and assistants.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6GCtiBek9mVBrbcRr3fWRw.png" /><figcaption>LangChain Ecosystem Architecture Diagram</figcaption></figure><h3>Why is LangChain Needed?</h3><p>LangChain addresses several key challenges in building LLM-powered applications:</p><ol><li><strong>Complexity Management</strong>: Building sophisticated LLM applications involves many moving parts. LangChain provides abstractions that make this manageable.</li><li><strong>LLM Orchestration</strong>: LLMs alone can’t solve complex problems that require multiple steps, tool use, or memory. LangChain provides frameworks for creating agents that can use tools, follow multi-step reasoning, and maintain context.</li><li><strong>Data Integration</strong>: LLMs have knowledge cutoffs and need access to external data. LangChain simplifies connecting LLMs to various data sources through retrieval-augmented generation (RAG).</li><li><strong>Development Velocity</strong>: The framework provides pre-built components that accelerate development, so you don’t need to reinvent common patterns.</li><li><strong>Observability and Debugging</strong>: LLM applications can be hard to debug. LangSmith provides visibility into what’s happening at each step.</li><li><strong>Deployment Readiness</strong>: Taking LLM prototypes to production requires handling state, streaming, and human-in-the-loop capabilities, which LangGraph Platform supports.</li></ol><p>The ecosystem gives developers the flexibility to build everything from simple chatbots to complex multi-agent systems that can reason, use tools, and work with external data sources.</p><h3>LangChain’s Architectural Evolution: A Developer’s Guide</h3><h4>The Major Architectural Shift</h4><p>LangChain underwent a significant architectural reorganization starting in late 2023, with major changes appearing in versions 0.0.300 through 0.1.0 (released in early 2024). This represented a fundamental shift from a monolithic structure to a modular, package-based architecture.Understanding the Package Reorganization</p><p>The reorganization split the original monolithic package into distinct modules with clear responsibilities:</p><ol><li><strong>langchain-core</strong>: Fundamental abstractions and interfaces</li><li><strong>langchain</strong>: Higher-level chains, agents, and orchestration</li><li><strong>langchain-community</strong>: Third-party integrations and community contributions</li><li><strong>Provider-specific packages</strong>: Dedicated integration packages for each LLM provider</li></ol><h4>Import Path Confusion: A Common Challenge</h4><p>The reorganization created confusion because identical or similar functionality appeared in multiple places. Here’s how to navigate this:</p><h4>Current Import Structure (As of March 2025):</h4><pre># ✅ RECOMMENDED IMPORTS<br><br># 1. Base abstractions<br>from langchain_core.prompts import PromptTemplate<br>from langchain_core.output_parsers import StructuredOutputParser<br>from langchain_core.runnables import Runnable<br><br># 2. LLM provider implementations<br>from langchain_openai import ChatOpenAI<br>from langchain_anthropic import ChatAnthropic<br><br># 3. Community integrations<br>from langchain_community.vectorstores import Chroma<br>from langchain_community.document_loaders import PyPDFLoader<br>from langchain_community.text_splitter import RecursiveCharacterTextSplitter<br><br># 4. High-level components<br>from langchain.chains import RetrievalQA<br>from langchain.agents import create_react_agent<br>from langchain.memory import ConversationBufferMemory</pre><h4>Legacy Import Structure (Deprecated):</h4><pre># ⚠️ DEPRECATED IMPORTS - Still work but will eventually be removed<br><br>from langchain.prompts import PromptTemplate  # Now in langchain_core.prompts<br>from langchain.llms import OpenAI  # Now in langchain_openai<br>from langchain.document_loaders import PyPDFLoader  # Now in langchain_community.document_loaders<br>from langchain.text_splitter import RecursiveCharacterTextSplitter  # Now in langchain_community.text_splitter</pre><h4>Import Decision Guide</h4><p>To avoid confusion, follow these simple rules:</p><ol><li><strong>For core abstractions</strong> (prompts, parsers, schemas): Use langchain_core.*</li><li><strong>For specific LLM providers</strong> (OpenAI, Anthropic, etc.): Use dedicated packages like langchain_openai</li><li><strong>For third-party integrations</strong> (document loaders, vector stores): Use langchain_community.*</li><li><strong>For high-level components</strong> (chains, agents, memory): Use langchain.*</li></ol><h4>Benefits of the New Architecture</h4><p>This reorganization provides clear advantages:</p><ul><li><strong>Reduced dependency bloat</strong>: Install only what you need</li><li><strong>Clearer versioning</strong>: Updates to one provider don’t affect others</li><li><strong>Better maintenance</strong>: Each package has focused responsibility</li><li><strong>Enhanced testing</strong>: Simpler, more focused test suites</li><li><strong>Clearer interfaces</strong>: Well-defined contracts between components</li></ul><h4>Practical Advice for Developers</h4><ol><li><strong>For new projects</strong>: Start with the new structure from day one</li><li><strong>For existing projects</strong>: Gradually migrate to the new imports during regular maintenance</li><li><strong>When facing errors</strong>: Check if you’re using a deprecated import path</li><li><strong>When reading documentation</strong>: Be aware of when documentation was written — older tutorials may use deprecated imports</li></ol><p>The temporary duplication of functionality exists to maintain backward compatibility during this transition period. While it creates short-term confusion, the long-term architectural benefits make this reorganization worthwhile for the ecosystem’s future.</p><h3>Choosing the Right Approach for Your Use Case</h3><p>LangChain offers multiple ways to build LLM applications, from simple direct model calls to complex orchestrated workflows. Here’s guidance on when to use each approach:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GnaHJHHMopJtbgoAfY_6NA.png" /><figcaption>LangChain Development Approaches Decision Guide</figcaption></figure><h4>1. Direct Component Integration</h4><p>Use when:</p><ul><li>You need fine-grained control over each step in your application</li><li>You want to understand the inner workings of each component</li><li>Your application has a linear flow with explicit data handoffs</li><li>You’re learning LangChain and want to see how each part functions independently</li><li>You’re building a prototype to validate specific component behaviors</li></ul><p>With this approach, you manually connect LangChain components (LLM providers, prompts, document loaders, text splitters, and vector databases) by writing code that explicitly handles the data flow between them. This gives you maximum visibility and control, though it requires more code than the other approaches.</p><h4>2. LCEL (LangChain Expression Language)</h4><p><strong>Use when:</strong></p><ul><li>You need to compose multiple components with a clean syntax</li><li>You want strong typing and structured inputs/outputs</li><li>You need streaming support across your entire chain</li><li>You want flexibility to swap components easily</li></ul><h4>3. LangGraph (for complex workflows)</h4><p><strong>Use when:</strong></p><ul><li>You need complex multi-step reasoning</li><li>Your workflow has conditional logic or loops</li><li>You’re building multi-agent systems</li><li>You need stateful processing</li><li>Human-in-the-loop functionality is required</li></ul><h3>Setting Up a LangChain Development Environment</h3><p>First, let’s set up <strong>pyenv</strong> for Python version management, followed by <strong>Poetry</strong> for dependency management. This guide for installing <strong>Poetry</strong> and <strong>pyenv</strong> is specifically for a <strong>Linux environment</strong>. Therefore, you can use either <strong>Windows WSL 2</strong> or a native Linux environment like <strong>Ubuntu</strong> or <strong>macOS</strong>.</p><p>If you’re using <strong>WSL</strong>, it is recommended to follow this tutorial with <strong>Ubuntu 20.04</strong> or <strong>22.04</strong>. For <strong>WSL</strong>, ensure you are using <strong>WSL 2</strong> with <strong>Ubuntu 22.04</strong> for optimal compatibility.</p><p>The YouTube video guide below will provide full instructions on how to set up <strong>WSL</strong> on a <strong>Windows 10 or 11</strong> environment. After setting it up, you can install the <strong>VS Code WSL extension</strong>, open the <strong>VS Code terminal</strong>, and follow all the steps as if you were in a native <strong>Linux environment</strong>.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F-atblwgc63E%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D-atblwgc63E&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F-atblwgc63E%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/90b1a3f2d9dfc13c243845778e94556a/href">https://medium.com/media/90b1a3f2d9dfc13c243845778e94556a/href</a></iframe><pre>####################################<br># Setting Up Pyenv<br><br># Install Pyenv<br>curl https://pyenv.run | bash<br><br># Add Pyenv to your shell configuration<br>echo &#39;export PYENV_ROOT=&quot;$HOME/.pyenv&quot;&#39; &gt;&gt; ~/.bashrc<br>echo &#39;command -v pyenv &gt;/dev/null || export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;&#39; &gt;&gt; ~/.bashrc<br>echo &#39;eval &quot;$(pyenv init -)&quot;&#39; &gt;&gt; ~/.bashrc<br><br># Reload shell configuration<br>source ~/.bashrc<br><br><br># List all available Python versions<br>pyenv install --list<br><br># Install Python 3.11 (Latest 3.11.10 will be installed)<br>pyenv install 3.11<br><br># verify installation<br>pyenv versions<br><br><br>####################################<br># Install Poetry<br><br># Install Poetry<br>curl -sSL https://install.python-poetry.org | python3 -<br><br># Add Poetry to PATH<br>echo &#39;export PATH=&quot;$HOME/.local/bin:$PATH&quot;&#39; &gt;&gt; ~/.bashrc<br>source ~/.bashrc<br><br># verify installation<br>poetry --version</pre><p>Moving forward, we will create the <strong>LangChain</strong> project.</p><pre># Set Python 3.11.10 as the local version for test project directory<br>mkdir langchain-project<br>cd langchain-project<br>pyenv local 3.11.10<br><br># Verify your active Python version<br>python --version<br><br># Verify Poetry installation<br>poetry init<br><br># Configure Poetry to create virtual environments in the project directory<br>poetry config virtualenvs.in-project true  <br><br># Create a new virtual environment<br>poetry env use python</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u0ANJwzoD3oLlEY5XmI0fA.png" /><figcaption>Creating the <strong>LangChain</strong> project</figcaption></figure><p>Let’s install <strong>LangChain</strong> and the required packages, and set up Jupyter Notebook support in <strong>VS Code</strong>.</p><pre># Install core LangChain packages<br>poetry add langchain langchain-core langchain-community<br><br># Install provider-specific packages based on your needs<br>poetry add langchain-openai  # For OpenAI models<br>poetry add langchain-google-genai  # For Google Gemini models<br>poetry add langchain-anthropic  # For Anthropic Claude models<br><br># Install LangGraph for complex workflows (if needed)<br>poetry add langgraph<br><br># Install additional useful packages<br>poetry add python-dotenv  # For environment variable management<br><br># Install Jupyter within your Poetry environment<br>poetry add ipykernel jupyter notebook</pre><p>Poetry will add the relevant dependencies to the <strong>pyproject.toml</strong> file, which you can review. Additionally, it will generate a <strong>poetry.lock</strong> file to ensure a deterministic build and consistent reproducibility across different environments.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eozg6qJ-Ujdp0n1LVVSMow.png" /><figcaption><strong>pyproject.toml and poetry.lock files</strong></figcaption></figure><p>Now, let’s set up the Python interpreter in <strong>VS Code</strong>.</p><p>• If you’re using <strong>macOS</strong>, press <strong>CMD + SHIFT + P</strong>.</p><p>• If you’re using <strong>Windows</strong>, press <strong>CTRL + SHIFT + P</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wuV41eEcf52vWP7Jx9c9fA.png" /><figcaption>Setup Python interpreter in VS Code</figcaption></figure><pre>crypto-trading-py3.11tharindu@192 langchain-project % poetry env info <br><br>Virtualenv<br>Python:         3.11.10<br>Implementation: CPython<br>Path:           /Users/tharindu/Library/Caches/pypoetry/virtualenvs/langchain-project-Luhyya8s-py3.11<br>Executable:     /Users/tharindu/Library/Caches/pypoetry/virtualenvs/langchain-project-Luhyya8s-py3.11/bin/python<br>Valid:          True<br><br>Base<br>Platform:   darwin<br>OS:         posix<br>Python:     3.11.10<br>Path:       /Users/tharindu/.pyenv/versions/3.11.10<br>Executable: /Users/tharindu/.pyenv/versions/3.11.10/bin/python3.11<br>crypto-trading-py3.11tharindu@192 langchain-project % </pre><p>Then, select the <strong>Python: Select Interpreter</strong> option. After that, enter the interpreter path, which you can retrieve using the poetry env info command. Under the <strong>“Executable”</strong> section, you will find the Python executable path. Copy this path, set it in VS Code, and confirm the selection.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*34mwdNqbx-gtzmDiOCtd7A.png" /><figcaption>Select Python Interpreter</figcaption></figure><p>Next, create a file named <strong>langchain-fundamental.ipynb</strong> in the directory and open it.</p><p>In the notebook interface, go to the <strong>top menu</strong> and select <strong>Kernel</strong>.</p><ul><li>Click <strong>Select Kernel</strong>, then choose the appropriate <strong>Python environment</strong>.</li><li>At the top, you should see the <strong>LangChain project’s Poetry environment</strong>. If necessary, remove any incorrect Python environment settings.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*21WeHH-Mx6LAQeG3_nUaCw.png" /><figcaption><strong>Select Jupyter Notebook Kernel</strong></figcaption></figure><p><strong>API Key Environment Variables</strong></p><p>Create a .env file in your project root and Edit the .env file to include your API keys:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4YXHSzEWYZmBpuXx4e4a5A.png" /><figcaption>.env file</figcaption></figure><p>When using different LLM providers in LangChain, the library automatically looks for specific environment variables to authenticate with each service:</p><pre># .env file content<br>OPENAI_API_KEY=&quot;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&quot;<br>GEMINI_API_KEY=&quot;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&quot;<br>ANTHROPIC_API_KEY=&quot;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&quot;</pre><p>First, we need to set up the .env file with the environment variables mentioned above. After that, before running any of the LLM provider commands below, we must load the environment to ensure the variables are properly set.</p><pre>import os<br>from dotenv import load_dotenv<br><br># Load environment variables<br>load_dotenv() <br><br># check OPENAI_API_KEY is set<br>if os.getenv(&quot;OPENAI_API_KEY&quot;) is None:<br>    print(&quot;OPENAI_API_KEY is not set&quot;)<br>    exit(1)<br>else:<br>    print(&quot;OPENAI_API_KEY is set&quot;)<br><br># check GEMINI_API_KEY is set<br>if os.getenv(&quot;GEMINI_API_KEY&quot;) is None:<br>    print(&quot;GEMINI_API_KEY is not set&quot;)<br>    exit(1)<br>else:<br>    print(&quot;GEMINI_API_KEY is set&quot;)<br><br># check ANTHROPIC_API_KEY is set<br>if os.getenv(&quot;ANTHROPIC_API_KEY&quot;) is None:<br>    print(&quot;ANTHROPIC_API_KEY is not set&quot;)<br>    exit(1)<br>else:<br>    print(&quot;ANTHROPIC_API_KEY is set&quot;)</pre><h3>Exploring Fundamental Components of LangChain</h3><p>To begin, let’s explore some simple use cases, such as how to utilize LangChain’s direct models from various LLM providers. Moving forward, we will also examine how LangChain prompts work, explore different document loaders, and delve into vector databases within LangChain. These components serve as the fundamental building blocks for developing LLM-powered applications.</p><p>With that in mind, let’s start by discussing different LLM providers. In future articles, we will cover the LangChain Expression Language (LCEL) and LangGraph for more complex use cases. For now, our focus will remain on individual components for basic and straightforward use cases.</p><h3>1. LLM Providers</h3><p>First things first, let’s consider a few major and widely used LLM providers. While there are many LLM providers available, for now, we will focus on langchain_openai,langchain_google_genai , and langchain_anthropic</p><h4>OpenAI Integration</h4><pre># Imports the LangChain wrapper for OpenAI&#39;s completion models<br>from langchain_openai import OpenAI<br><br># Initialization checks for the OPENAI_API_KEY environment variable<br>llm = OpenAI(model=&quot;gpt-3.5-turbo-instruct&quot;)<br>llm.invoke(&quot;who is Lanfear?&quot;)<br><br># Sends a single prompt to the model and returns the complete response<br>llm.invoke(&quot;What is the capital of France?&quot;)<br><br># Efficiently processes multiple prompts in one API call where possible<br>llm.batch([&quot;Who is Lanfear?&quot;, &quot;Who is Dragon Reborn?&quot;, &quot;What is one Power?&quot;])<br><br># Returns a generator that yields response tokens as they&#39;re generated, enabling real-time display<br>for chunk in llm.stream(&quot;Who is Lanfear?&quot;):<br>    print(chunk, end=&quot;&quot;, flush=True)</pre><h4>Google Generative AI (Gemini) Integration</h4><pre># imports the wrapper for Google&#39;s Gemini models<br>from langchain_google_genai import GoogleGenerativeAI<br><br># Looks for GOOGLE_API_KEY or GEMINI_API_KEY environment variable<br>llm = GoogleGenerativeAI(model=&quot;gemini-2.0-flash&quot;)<br>llm.invoke(&quot;who is Lanfear?&quot;)</pre><h4>Anthropic Integration</h4><pre># imports the wrapper for Anthropic&#39;s models.<br>from langchain_anthropic import ChatAnthropic<br><br># Looks for the ANTHROPIC_API_KEY environment variable<br>llm = ChatAnthropic(model=&quot;claude-3-sonnet-20240229&quot;)<br>llm.invoke(&quot;who is Lanfear?&quot;)</pre><h4>Key Differences Among Providers</h4><p><strong>Model Names</strong>: Each provider uses its own naming convention for models</p><ul><li>OpenAI: gpt-3.5-turbo-instruct, gpt-4, etc.</li><li>Google: gemini-2.0-flash, gemini-2.0-pro, etc.</li><li>Anthropic: claude-3-sonnet-20240229, claude-3-opus-20240229, etc.</li></ul><p><strong>Chat vs. Completion Models</strong>:</p><ul><li>OpenAI example uses the completion API (OpenAI)</li><li>For chat models, you would use ChatOpenAI instead</li><li>Anthropic example uses ChatAnthropic which is designed for Claude&#39;s chat interface</li></ul><p><strong>Parameter Differences</strong>:</p><ul><li>While the basic interface is standardized, each provider may support different parameters</li><li>For example, Anthropic has a max_tokens_to_sample parameter rather than max_tokens</li></ul><h4>Behind the Scenes</h4><p>When you call methods like invoke(), LangChain:</p><ol><li>Formats your input into the provider-specific API format</li><li>Handles authentication with your API key</li><li>Makes the API request to the provider</li><li>Processes the response into a standardized format</li><li>If LangSmith tracing is enabled, logs the request and response</li><li>Returns the result in a consistent way regardless of provider</li></ol><p>This abstraction layer is what makes LangChain powerful — it provides a unified interface across different LLM providers while still allowing access to provider-specific features when needed.</p><h3>2. LangChain Prompts</h3><p>Next, let’s explore another key concept of <strong>LangChain</strong>, which is the <strong>LangChain Prompting System</strong>. This system is particularly useful for creating dynamic prompts for large language models.</p><h4>Basic PromptTemplate Usage</h4><pre>from langchain_core.prompts import PromptTemplate<br><br>template = PromptTemplate(<br>    input_variables=[&quot;name&quot;, &quot;age&quot;],<br>    template=&quot;My name is {name} and I am {age} years old.&quot;<br>)<br># Format the template with values<br>formatted = template.format(name=&quot;Alice&quot;, age=25)<br># Result: &quot;My name is Alice and I am 25 years old.&quot;</pre><p>This creates a prompt template with two variables (name and age) and then fills them with specific values.</p><h4>Quick Template Creation</h4><pre>PromptTemplate.from_template(&quot;My Name is {Name}&quot;).format(Name=&quot;Tharindu&quot;)<br># Result: &quot;My Name is Tharindu&quot;</pre><p>The from_template method provides a shortcut to create a template by automatically detecting the variables in curly braces.</p><h4>Batch Processing</h4><pre>PromptTemplate.from_template(&quot;My Name is {Name}&quot;).batch([{&quot;Name&quot;:&quot;Tharindu&quot;},{&quot;Name&quot;:&quot;John&quot;}])<br># Result: [&quot;My Name is Tharindu&quot;, &quot;My Name is John&quot;]</pre><p>The batch method lets you apply multiple sets of values to the same template, returning a list of formatted strings.</p><h4>Single-Variable Templates</h4><pre>prompt_template = PromptTemplate.from_template(&quot;Explain me about {topic}&quot;)<br>prompt_template.format(topic=&quot;Wheel of Time Series&quot;)<br># Result: &quot;Explain me about Wheel of Time Series&quot;</pre><p>Here, a simple template with one variable is created and filled.</p><h4>Simplified Batch Processing</h4><pre>prompt_template.batch([&quot;Lanfear&quot;, &quot;Rand&quot;, &quot;One Power&quot;])<br># Result: [&quot;Explain me about Lanfear&quot;, &quot;Explain me about Rand&quot;, &quot;Explain me about One Power&quot;]</pre><p>For single-variable templates, you can pass a simple list of values to batch instead of a list of dictionaries.</p><h4>Creating and Batching in One Step</h4><pre>PromptTemplate.from_template(&quot;Tell me a joke about {topic}&quot;).batch([&quot;cats&quot;, &quot;dogs&quot;, &quot;birds&quot;])<br># Result: [&quot;Tell me a joke about cats&quot;, &quot;Tell me a joke about dogs&quot;, &quot;Tell me a joke about birds&quot;]</pre><p>You can create a template and apply batch formatting in one operation.</p><h4>Alternative Batch Syntax</h4><pre>PromptTemplate(template=&quot;Tell me a joke about {topic}&quot;).batch([&quot;cats&quot;, &quot;dogs&quot;, &quot;birds&quot;])<br># Result: [&quot;Tell me a joke about cats&quot;, &quot;Tell me a joke about dogs&quot;, &quot;Tell me a joke about birds&quot;]</pre><p>This shows the equivalent using the constructor syntax instead of from_template.</p><h4>Multiple Input Variables</h4><pre>template = PromptTemplate(<br>    input_variables=[&quot;context&quot;, &quot;question&quot;],<br>    template=&quot;&quot;&quot;<br>    Context: {context}<br>    <br>    Question: {question}<br>    <br>    Answer the question based on the context above.<br>    &quot;&quot;&quot;<br>)</pre><p>Templates can have multiple variables and support multi-line strings for more complex prompts.</p><h4>Partial Variables</h4><pre>template = PromptTemplate(<br>    input_variables=[&quot;question&quot;],<br>    partial_variables={&quot;format&quot;: &quot;markdown&quot;},<br>    template=&quot;Format the answer in {format}:\n{question}&quot;<br>)</pre><p>Partial variables are pre-filled values that remain constant across different uses of the template.</p><h4>Template Validation</h4><pre>template = PromptTemplate(<br>    input_variables=[&quot;product&quot;],<br>    template=&quot;Tell me about {product}&quot;,<br>    validate_template=True  # Validates all variables are present<br>)</pre><p>Setting validate_template=True checks that all variables in the template string are included in input_variables.</p><h4>Accessing Template Properties</h4><pre># Access template string<br>print(template.template)  # &quot;Tell me about {product}&quot;<br><br># Get input variables<br>print(template.input_variables)  # [&quot;product&quot;]<br># Get partial variables<br>print(template.partial_variables)  # {}</pre><p>These methods let you inspect the components of a template.</p><h4>Chat Prompt Templates</h4><pre>from langchain_core.prompts import ChatPromptTemplate<br><br>prompt_template = ChatPromptTemplate([<br>    (&quot;system&quot;, &quot;You are a helpful assistant&quot;),<br>    (&quot;user&quot;, &quot;Tell me a joke about {topic}&quot;)<br>])<br>prompt_template.invoke({&quot;topic&quot;: &quot;cats&quot;})</pre><p>ChatPromptTemplate creates prompts specifically designed for chat models, with distinct message types (system, user, etc.).</p><h4>Dynamic Message Placeholders</h4><pre>from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder<br>from langchain_core.messages import HumanMessage<br><br>prompt_template = ChatPromptTemplate([<br>    (&quot;system&quot;, &quot;You are a helpful assistant&quot;),<br>    MessagesPlaceholder(&quot;msgs&quot;)<br>])<br>prompt_template.invoke({&quot;msgs&quot;: [HumanMessage(content=&quot;hi!&quot;)]})</pre><p>MessagesPlaceholder allows you to dynamically insert multiple messages into a chat template.</p><h4>Message-Specific Templates</h4><pre>from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate<br><br><br># Simple Prompt Template<br>prompt_template = PromptTemplate.from_template(&quot;Tell me a joke about {topic}&quot;)<br>formatted_prompt = prompt_template.format(topic=&quot;cats&quot;)<br># Chat Prompt Template<br>chat_prompt_template = ChatPromptTemplate.from_messages([<br>    SystemMessagePromptTemplate.from_template(&quot;You are a helpful assistant that tells jokes.&quot;),<br>    HumanMessagePromptTemplate.from_template(&quot;{topic} joke?&quot;)<br>])<br>formatted_chat_prompt = chat_prompt_template.format_messages(topic=&quot;dogs&quot;)</pre><p>This shows how to create specific message types (system, human) for chat templates.</p><h4>Dynamic Template Selection</h4><pre>def create_prompt(condition):<br>    if condition:<br>        template = &quot;Do task A: {input}&quot;<br>    else:<br>        template = &quot;Do task B: {input}&quot;<br>    return PromptTemplate.from_template(template)<br><br><br>prompt_a = create_prompt(True)<br>prompt_b = create_prompt(False)</pre><p>You can dynamically select different templates based on conditions in your code.</p><h4>Few-Shot Learning Templates</h4><pre>template = &quot;&quot;&quot;<br>Example 1: Input: I love cats; Output: Positive<br>Example 2: Input: I hate Mondays; Output: Negative<br>Input: {text}; Output:&quot;&quot;&quot;<br><br>prompt = PromptTemplate.from_template(template)</pre><p>Templates can include examples to guide the model’s responses (few-shot learning).</p><h4>Structured Prompt Templates</h4><pre>template = &quot;&quot;&quot;<br>Instructions: {instructions}<br>Context: {context}<br>Question: {question}<br>Answer:&quot;&quot;&quot;<br><br><br>prompt = PromptTemplate.from_template(template)</pre><p>Using a consistent structure with multiple components (instructions, context, question) makes prompts more effective and readable.</p><p>These examples demonstrate the flexibility of LangChain’s prompt templating system for creating dynamic, reusable prompts for language models.</p><h3><strong>3. Document Loaders</strong></h3><p>Next, let’s explore another important concept in <strong>LangChain</strong>: <strong>LangChain Document Loaders</strong>.</p><p>Using <strong>Document Loaders</strong>, we can load various types of documents, including <strong>PDFs, EPUB files, YouTube videos, text files, CSV files,</strong> and many others.</p><p>LangChain Document Loaders are powerful tools that help you import various types of content into your LangChain applications. Let’s explore how they work with examples and explanations.</p><h4>YouTube Loader Example</h4><pre># install the library<br>poetry add youtube-transcript-api</pre><pre>from langchain.document_loaders import YoutubeLoader<br>from langchain.text_splitter import RecursiveCharacterTextSplitter<br><br># 1. Load the YouTube video transcript<br>loader = YoutubeLoader.from_youtube_url(https://medium.com/&quot;https://www.youtube.com/watch?v=zx6T88Bl4p0&quot;)<br>documents = loader.load()<br># 2. Split the transcript into manageable chunks<br>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,  # Adjust chunk size as needed<br>    chunk_overlap=0   # Adjust overlap as needed<br>)<br>docs = text_splitter.split_documents(documents)</pre><p>This code:</p><ol><li>Imports the YoutubeLoader class</li><li>Creates a loader for a specific YouTube video URL</li><li>Extracts the transcript as a document</li><li>Splits the document into smaller chunks for processing</li></ol><h4>Common Document Loaders</h4><p>Here are examples of other popular document loaders:</p><h4>PDF Loader</h4><pre># install the library<br>poetry add pip install pypdf</pre><pre>from langchain.document_loaders import PyPDFLoader<br><br># Load a PDF file<br>loader = PyPDFLoader(&quot;data/The Great Hunt.pdf&quot;)<br>pages = loader.load()<br># Each page is a Document with page content and metadata<br>for page in pages:<br>    print(page.page_content[:100])  # Print first 100 chars of each page<br>    print(f&quot;Page: {page.metadata[&#39;page&#39;]}&quot;)  # Access page number from metadata</pre><h4>CSV Loader</h4><pre>from langchain.document_loaders.csv_loader import CSVLoader<br><br># Load a CSV file<br>loader = CSVLoader(<br>    file_path=&quot;data/split_chunks.csv&quot;,<br>    csv_args={<br>        &#39;delimiter&#39;: &#39;,&#39;,<br>        &#39;quotechar&#39;: &#39;&quot;&#39;,<br>    }<br>)<br>documents = loader.load()<br># Each row becomes a document<br>print(f&quot;Loaded {len(documents)} documents&quot;)<br>print(documents[0].page_content)  # First row content</pre><h4>Text File Loader</h4><pre>from langchain.document_loaders import TextLoader<br><br># Load a text file<br>loader = TextLoader(&quot;data/sample-file.txt&quot;)<br>documents = loader.load()<br>print(documents[0].page_content[:200])  # Print first 200 chars</pre><h4>Web Page Loader</h4><pre>from langchain.document_loaders import WebBaseLoader<br><br># Load content from a web page<br>loader = WebBaseLoader(&quot;https://langchain-ai.github.io/langgraph/tutorials/&quot;)<br>documents = loader.load()<br>print(f&quot;Loaded {len(documents)} web pages&quot;)<br>documents</pre><h4>EPUB Loader</h4><pre># install the library<br>poetry add &quot;unstructured[epub]&quot;</pre><pre><br>from langchain.document_loaders import UnstructuredEPubLoader<br><br># Load an EPUB file<br>loader = UnstructuredEPubLoader(&quot;data/01. The Eye of the World.epub&quot;)<br>documents = loader.load()<br>print(f&quot;Extracted {len(documents)} document segments from the EPUB&quot;)</pre><h4>Directory Loader</h4><pre>from langchain.document_loaders import DirectoryLoader<br><br># Load all text files from a directory<br>loader = DirectoryLoader(<br>    &#39;data/&#39;, <br>    glob=&#39;**/*.txt&#39;,  # Load all .txt files, including in subdirectories<br>    loader_cls=TextLoader  # Use TextLoader for each file<br>)<br>documents = loader.load()<br>print(f&quot;Loaded {len(documents)} documents from directory&quot;)</pre><h4>Working with Multiple Document Types</h4><p>For complex applications, you might need to load multiple document types:</p><pre>from langchain.document_loaders import PyPDFLoader, TextLoader, CSVLoader<br>from langchain.text_splitter import RecursiveCharacterTextSplitter<br><br># Load different document types<br>pdf_loader = PyPDFLoader(&quot;./data/The Great Hunt.pdf&quot;)<br>text_loader = TextLoader(&quot;./data/sample-file.txt&quot;)<br>csv_loader = CSVLoader(&quot;./data/split_chunks.csv&quot;)<br># Combine all documents<br>all_documents = []<br>all_documents.extend(pdf_loader.load())<br>all_documents.extend(text_loader.load())<br>all_documents.extend(csv_loader.load())<br># Split all documents consistently<br>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,<br>    chunk_overlap=200<br>)<br>split_documents = text_splitter.split_documents(all_documents)<br>print(f&quot;Loaded and split {len(split_documents)} document chunks&quot;)</pre><h4>Custom Document Loaders</h4><p>You can create custom loaders for specialized formats:</p><pre>from langchain.docstore.document import Document<br>from langchain.document_loaders.base import BaseLoader<br>import json<br>import os<br><br># Define the custom loader class<br>class CustomJSONLoader(BaseLoader):<br>    &quot;&quot;&quot;Load JSON files with a custom structure.&quot;&quot;&quot;<br>    <br>    def __init__(self, file_path):<br>        self.file_path = file_path<br>    <br>    def load(self):<br>        import json<br>        with open(self.file_path, &#39;r&#39;) as f:<br>            data = json.load(f)<br>        <br>        # Convert JSON data to documents<br>        documents = []<br>        for item in data[&#39;items&#39;]:<br>            # Create a document for each item<br>            content = f&quot;Title: {item[&#39;title&#39;]}\nDescription: {item[&#39;description&#39;]}&quot;<br>            metadata = {&quot;source&quot;: self.file_path, &quot;id&quot;: item[&#39;id&#39;]}<br>            documents.append(Document(page_content=content, metadata=metadata))<br>            <br>        return documents<br># Create sample JSON file for testing<br>sample_data = {<br>    &quot;items&quot;: [<br>        {<br>            &quot;id&quot;: &quot;item1&quot;,<br>            &quot;title&quot;: &quot;Introduction to LangChain&quot;,<br>            &quot;description&quot;: &quot;A beginner&#39;s guide to using LangChain for document processing.&quot;<br>        },<br>        {<br>            &quot;id&quot;: &quot;item2&quot;,<br>            &quot;title&quot;: &quot;Custom Loaders in LangChain&quot;,<br>            &quot;description&quot;: &quot;How to create and use custom document loaders in your projects.&quot;<br>        },<br>        {<br>            &quot;id&quot;: &quot;item3&quot;,<br>            &quot;title&quot;: &quot;Advanced LLM Applications&quot;,<br>            &quot;description&quot;: &quot;Building sophisticated applications using language models and document loaders.&quot;<br>        }<br>    ]<br>}<br># Write sample data to a file<br>with open(&quot;sample_data.json&quot;, &quot;w&quot;) as f:<br>    json.dump(sample_data, f, indent=2)<br># Use the custom loader<br>custom_loader = CustomJSONLoader(&quot;sample_data.json&quot;)<br>documents = custom_loader.load()<br># Display the loaded documents<br>print(f&quot;Loaded {len(documents)} documents:&quot;)<br>for i, doc in enumerate(documents):<br>    print(f&quot;\nDocument {i+1}:&quot;)<br>    print(f&quot;Content: {doc.page_content}&quot;)<br>    print(f&quot;Metadata: {doc.metadata}&quot;)<br># Clean up the sample file<br>os.remove(&quot;sample_data.json&quot;)</pre><p><strong>Key Concepts of Document Loaders</strong></p><ol><li><strong>Documents</strong> — In LangChain, a document has two components:</li></ol><ul><li>page_content: The actual text content</li><li>metadata: Additional information about the document (source, page number, etc.)</li></ul><p><strong>2. Text Splitting</strong> — Documents are often too large for language models, so they need to be split into smaller chunks.</p><p><strong>3. Metadata Preservation</strong> — Good document loaders maintain relevant metadata during loading and splitting.</p><p><strong>4. Content Extraction</strong> — Different loaders handle different extraction challenges (parsing PDFs, extracting transcripts, etc.).</p><p>This overview covers the core functionality of LangChain Document Loaders and how to use them effectively in your applications.</p><h3>4. Text Splitters</h3><p>Text splitters are a crucial component in LangChain, responsible for breaking down large documents into manageable chunks that can be processed by language models. Let’s explore how they work, focusing on the <strong>RecursiveCharacterTextSplitter</strong> and other options.</p><h4>Why We Need to Chunk Documents</h4><p>Before diving into the specifics, it’s important to understand why chunking is necessary:</p><ol><li><strong>Token Limitations</strong>: Language models have context window limitations (typically 4K-32K tokens). Large documents often exceed these limits.</li><li><strong>Relevance Precision</strong>: Smaller, focused chunks help retrieval systems find exactly the information needed rather than returning entire documents.</li><li><strong>Processing Efficiency</strong>: Working with smaller chunks reduces memory usage and computational requirements.</li><li><strong>Cost Optimization</strong>: When using commercial APIs like OpenAI, smaller chunks mean fewer tokens processed, reducing costs.</li></ol><h4>Main Text Splitters in LangChain</h4><p>LangChain offers several text splitters:</p><pre>from langchain.text_splitter import (<br>    RecursiveCharacterTextSplitter,<br>    CharacterTextSplitter,<br>    TokenTextSplitter,<br>    MarkdownTextSplitter,<br>    HTMLTextSplitter<br>)</pre><h4>RecursiveCharacterTextSplitter</h4><p>This is the most commonly used and recommended splitter:</p><pre>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,<br>    chunk_overlap=200,<br>    length_function=len,<br>    separators=[&quot;\n\n&quot;, &quot;\n&quot;, &quot; &quot;, &quot;&quot;]<br>)</pre><h4>How It Works:</h4><ol><li>It attempts to split text on a hierarchy of separators, starting with the most significant ones (like paragraph breaks).</li><li>It works recursively — if a chunk is still too large after splitting on one separator, it moves to the next separator in the list.</li><li>The default separator list is [&quot;\n\n&quot;, &quot;\n&quot;, &quot; &quot;, &quot;&quot;] - attempting to maintain coherent paragraphs when possible.</li></ol><p>Example usage:</p><pre>from langchain.text_splitter import RecursiveCharacterTextSplitter<br>from langchain.document_loaders import PyPDFLoader<br><br># Load a document<br>loader = PyPDFLoader(&quot;data/The Great Hunt.pdf&quot;)<br>document = loader.load()[10]<br># Initialize the splitter<br>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,<br>    chunk_overlap=200<br>)<br># Split the document<br>chunks = text_splitter.split_documents([document])<br>print(f&quot;Split into {len(chunks)} chunks&quot;)<br>chunks[0].page_content</pre><h4>CharacterTextSplitter</h4><p>A simpler alternative that splits only on specified characters:</p><pre>splitter = CharacterTextSplitter(<br>    separator=&quot;\n\n&quot;,<br>    chunk_size=1000,<br>    chunk_overlap=200<br>)</pre><p>This splits only on the exact separator provided, without the recursive approach.</p><h4>TokenTextSplitter</h4><p>Splits text based on token count rather than character count:</p><pre>from langchain.text_splitter import TokenTextSplitter<br><br>splitter = TokenTextSplitter(<br>    chunk_size=500,  # tokens, not characters<br>    chunk_overlap=50<br>)</pre><p>This is useful when you need precise control over token counts for specific models.</p><h4>Format-Specific Splitters</h4><p>LangChain also provides specialized splitters for particular formats:</p><pre># For Markdown documents<br>markdown_splitter = MarkdownTextSplitter(chunk_size=1000, chunk_overlap=200)</pre><p>These respect the structure of the specific formats, splitting at appropriate headings and sections.</p><h4>Key Parameters Explained</h4><h4>Chunk Size and Overlap</h4><pre>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,  # Maximum size of each chunk in characters<br>    chunk_overlap=200  # Number of characters to overlap between chunks<br>)</pre><ul><li><strong>chunk_size</strong>: Maximum size (in characters or tokens) for each chunk. If you’re using an embedding model with a 512 token limit, a good rule of thumb is to set chunk_size to around 400–450 characters to leave room for the query.</li><li><strong>chunk_overlap</strong>: Number of characters that overlap between consecutive chunks. This helps maintain context between chunks and prevents concepts from being split.</li></ul><h4>When to Adjust These Parameters</h4><p><strong>Increase chunk_size</strong> when:</p><ul><li>Documents have long, coherent sections that shouldn’t be split</li><li>You’re using models with larger context windows</li><li>You need more context in each chunk for better understanding</li></ul><p><strong>Increase chunk_overlap</strong> when:</p><ul><li>You notice context is being lost between chunks</li><li>Important concepts are being split across chunks</li><li>Documents have important cross-references</li></ul><p><strong>Decrease chunk_size</strong> when:</p><ul><li>You’re hitting token limits with your model</li><li>You need more precise retrieval</li><li>You want to reduce costs with API-based models</li></ul><h4>When Chunk Size Is Exceeded</h4><p>If a single chunk exceeds the specified size even after splitting on all separators:</p><ol><li>The RecursiveCharacterTextSplitter will force a split at the exact chunk size</li></ol><h4>Best Practices</h4><ol><li><strong>Test Different Splitting Strategies</strong>: No single configuration works for all document types.</li><li><strong>Consider Document Structure</strong>: Choose splitters that respect the natural structure of your documents.</li><li><strong>Balance Chunk Size and Overlap</strong>: Too large chunks waste tokens, too small chunks lose context.</li><li><strong>Monitor Performance</strong>: Track retrieval quality with different splitting configurations.</li><li><strong>Combine with Metadata</strong>: Enhance chunks with metadata (section titles, document sources) to improve retrieval context.</li></ol><p>By understanding text splitters and configuring them appropriately, you can significantly improve the performance of your LangChain applications, particularly for retrieval-augmented generation (RAG) systems.</p><h3>5. Vector Databases</h3><p>Vector databases are a crucial component of the modern LLM application stack, enabling efficient storage and retrieval of embeddings for semantic search and retrieval-augmented generation (RAG). They solve the challenge of finding semantically similar content across large document collections by using embedding vectors and similarity search algorithms.</p><p>In this section, we’ll explore how LangChain integrates with popular vector databases, including FAISS, Pinecone, Weaviate, and Milvus.</p><h4>Understanding Vector Databases</h4><p>Vector databases specialize in storing high-dimensional vectors (embeddings) and performing similarity searches. When you convert text to embeddings using models like OpenAI’s text-embedding-3-large, you get numerical representations that capture semantic meaning. Vector databases can quickly find the most similar vectors to a query vector using distance metrics like cosine similarity, Euclidean distance, or dot product.</p><p>Key benefits include:</p><ul><li><strong>Semantic search</strong>: Find documents based on meaning, not just keywords</li><li><strong>Efficient similarity search</strong>: Specialized data structures for high-dimensional search</li><li><strong>Metadata filtering</strong>: Search within specific document categories or attributes</li><li><strong>Scalability</strong>: Handle millions to billions of vectors efficiently</li></ul><h4>FAISS — Facebook AI Similarity Search</h4><p>FAISS is an open-source library developed by Facebook Research that provides efficient similarity search and clustering of dense vectors. It’s particularly useful for local development or self-hosted solutions.</p><pre>poetry add faiss-cpu</pre><h4>Creating and Saving a FAISS Index</h4><pre>from langchain.text_splitter import RecursiveCharacterTextSplitter<br>from langchain.document_loaders import PyPDFLoader<br><br># Load a document<br>loader = PyPDFLoader(&quot;data/The Great Hunt.pdf&quot;)<br>document = loader.load()<br>print(f&quot;Loaded document with {len(document)} characters&quot;)<br><br># Initialize the splitter<br>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,<br>    chunk_overlap=200<br>)<br># Split the document<br>chunks = text_splitter.split_documents(document)<br>print(f&quot;Split into {len(chunks)} chunks&quot;)</pre><pre>import faiss<br>import numpy as np<br>from langchain_community.vectorstores import FAISS<br>from langchain_openai import OpenAIEmbeddings<br><br># Initialize the embeddings model<br>embeddings = OpenAIEmbeddings(model=&quot;text-embedding-3-large&quot;)<br><br># Create from documents<br>db = FAISS.from_documents(chunks, embeddings)<br><br># Save the FAISS index<br>db.save_local(&quot;./data/faiss_youtube_index&quot;)</pre><h4>Customizing the FAISS Index</h4><p>You can customize the FAISS index with various parameters:</p><pre>db = FAISS.from_documents(<br>    documents,<br>    embeddings,<br>    normalize_L2=False,  # L2 normalization<br>    distance_strategy=&quot;COSINE&quot;,  # or &quot;INNER_PRODUCT&quot;, &quot;EUCLIDEAN&quot;<br>)</pre><h4>Loading an Existing FAISS Index</h4><p>FAISS saves indexes as pickle files and can potentially contain malicious code.</p><pre>langchain-project-py3.11tharindu@Tharindus-MacBook-Pro langchain-project % ls -ltrh ./data/faiss_youtube_index<br>total 11120<br>-rw-r--r--@ 1 tharindu  staff   4.0M Mar 17 16:35 index.faiss<br>-rw-r--r--@ 1 tharindu  staff   1.4M Mar 17 16:35 index.pkl<br>langchain-project-py3.11tharindu@Tharindus-MacBook-Pro langchain-project %</pre><p>The security measure in LangChain when loading FAISS indexes from pickle files will give warnings. The `allow_dangerous_deserialization` flag is required when loading from pickle files.</p><pre># Load existing index<br>db = FAISS.load_local(<br>    &quot;./data/faiss_youtube_index&quot;, <br>    embeddings,<br>    allow_dangerous_deserialization=True<br>)</pre><h4>Performing Similarity Search</h4><pre># Basic similarity search - returns documents<br>results = db.similarity_search(&quot;Who is Rand?&quot;, k=4)<br><br># Search with scores - returns documents and their similarity scores<br>results_with_scores = db.similarity_search_with_score(&quot;Who is Rand?&quot;, k=4)</pre><h3>Pinecone</h3><p>Pinecone is a managed vector database service that offers high scalability and performance. It’s a good choice for production applications requiring low latency and high availability.</p><p>​Pinecone offers a free Starter plan designed for developers to explore and build applications with vector databases. For a visual walkthrough, you can refer to this tutorial vidoe to setting up a Free Pinecone Account.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FkTUnOQmrgAc%3Fstart%3D2%26feature%3Doembed%26start%3D2&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DkTUnOQmrgAc&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FkTUnOQmrgAc%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/19ef4dddf3b3b5bb3ae7d5442d45a990/href">https://medium.com/media/19ef4dddf3b3b5bb3ae7d5442d45a990/href</a></iframe><p>The Starter plan includes the following limits:​</p><ul><li>Storage: Up to 2 GB per project.​</li><li>Write Operations: Up to 2 million write units per month.​</li><li>Read Operations: Up to 1 million read units per month.​</li><li>Indexes: Up to 5 indexes per project.​</li><li>Namespaces per Index: Up to 100 namespaces.​</li><li>Embedding Models: Access to all available models.​</li><li>Reranking Models: Access to all except cohere-rerank-3.5.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qGPr5eXLq4BzmRlcC_XPZg.png" /><figcaption>Pinecone starter plan usage limits</figcaption></figure><h4>Setting Up Pinecone</h4><pre>from langchain.text_splitter import RecursiveCharacterTextSplitter<br>from langchain.document_loaders import PyPDFLoader<br><br># Load a document<br>loader = PyPDFLoader(&quot;data/The Great Hunt.pdf&quot;)<br>document = loader.load()<br>print(f&quot;Loaded document with {len(document)} characters&quot;)<br><br># Initialize the splitter<br>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,<br>    chunk_overlap=200<br>)<br># Split the document<br>chunks = text_splitter.split_documents(document)<br>print(f&quot;Split into {len(chunks)} chunks&quot;)</pre><pre>poetry add langchain-pinecone</pre><pre>from langchain_pinecone import PineconeVectorStore<br>from pinecone import Pinecone, ServerlessSpec<br>import os<br>import time<br><br># Initialize Pinecone client<br>pc = Pinecone(api_key=os.environ.get(&quot;PINECONE_API_KEY&quot;))<br><br># Create a new index if it doesn&#39;t exist<br>index_name = &quot;wheel-of-time-the-great-hunt&quot;<br>existing_indexes = [index_info[&quot;name&quot;] for index_info in pc.list_indexes()]<br><br>if index_name not in existing_indexes:<br>    pc.create_index(<br>        name=index_name,<br>        dimension=3072,  # Match your embedding model&#39;s dimension<br>        metric=&quot;cosine&quot;,<br>        spec=ServerlessSpec(cloud=&quot;aws&quot;, region=&quot;us-east-1&quot;),<br>        deletion_protection=&quot;enabled&quot;,  # Defaults to &quot;disabled&quot;<br>    )<br>    # Wait for the index to be ready<br>    while not pc.describe_index(index_name).status[&quot;ready&quot;]:<br>        time.sleep(1)<br><br># Connect to the index<br>index = pc.Index(index_name)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wNofTvlu2WW1NafMRQjaPA.png" /><figcaption>wheel-of-time-the-great-hunt index created</figcaption></figure><h4>Uploading Documents with Metadata</h4><p>Pinecone supports metadata filtering, which allows for more sophisticated querying:</p><pre>chunks[0].metadata<br><br>{&#39;producer&#39;: &#39;Python PDF Library - http://pybrary.net/pyPdf/&#39;,<br> &#39;creator&#39;: &#39;PyPDF&#39;,<br> &#39;creationdate&#39;: &#39;&#39;,<br> &#39;source&#39;: &#39;data/The Great Hunt.pdf&#39;,<br> &#39;total_pages&#39;: 343,<br> &#39;page&#39;: 0,<br> &#39;page_label&#39;: &#39;1&#39;}</pre><pre><br># Initialize embedding model<br>embedding = OpenAIEmbeddings(model=&quot;text-embedding-3-large&quot;)<br><br># Create vector store with the embedding model<br>vector_store = PineconeVectorStore(index=index, embedding=embedding)<br><br># Prepare the document for upload<br>texts = []<br>metadatas = []<br>ids = []<br><br>for i, doc in enumerate(chunks):  # Assuming &#39;document&#39; is your collection of Document objects<br>    texts.append(doc.page_content)<br>    metadatas.append({<br>        &quot;source&quot;: doc.metadata.get(&quot;source&quot;, &quot;&quot;),<br>        &quot;producer&quot;: doc.metadata.get(&quot;producer&quot;, &quot;&quot;),<br>        &quot;creator&quot;: doc.metadata.get(&quot;creator&quot;, &quot;&quot;),<br>        &quot;total_pages&quot;: doc.metadata.get(&quot;total_pages&quot;, 0),<br>        &quot;page&quot;: doc.metadata.get(&quot;page&quot;, 0),<br>        &quot;page_label&quot;: doc.metadata.get(&quot;page_label&quot;, &quot;&quot;),<br>        &quot;title&quot;: &quot;The Great Hunt&quot;,<br>        &quot;author&quot;: &quot;Robert Jordan&quot;<br>    })<br>    ids.append(f&quot;chunk_{i}&quot;)<br><br># Upload document in batches<br>batch_size = 50<br>for i in range(0, len(texts), batch_size):<br>    end_idx = min(i + batch_size, len(texts))<br>    vector_store.add_texts(<br>        texts=texts[i:end_idx],<br>        metadatas=metadatas[i:end_idx],<br>        ids=ids[i:end_idx]<br>    )<br>    print(f&quot;Uploaded chunks {i} to {end_idx-1}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZL6bvylQK-3xI8xmqfRP1w.png" /><figcaption>Exploring the Metadata of Indexes in the Pinecone Web UI</figcaption></figure><h4>Querying with Pinecone</h4><pre># Simple similarity search<br>vector_store.similarity_search(&quot;Who is Rand?&quot;, k=4)<br><br># Similarity search with scores<br>vector_store.similarity_search_with_score(&quot;Who is Rand?&quot;, k=10)<br><br># Search with metadata filtering<br>vector_store.similarity_search(<br>    &quot;Who is Rand?&quot;, <br>    k=10,<br>    filter={&quot;page_label&quot;: &#39;240&#39;}<br>)</pre><pre>[Document(id=&#39;chunk_1293&#39;, metadata={&#39;author&#39;: &#39;Robert Jordan&#39;, &#39;creator&#39;: &#39;PyPDF&#39;, &#39;page&#39;: 239.0, &#39;page_label&#39;: &#39;240&#39;, &#39;producer&#39;: &#39;Python PDF Library - http://pybrary.net/pyPdf/&#39;, &#39;source&#39;: &#39;data/The Great Hunt.pdf&#39;, &#39;title&#39;: &#39;The Great Hunt&#39;, &#39;total_pages&#39;: 343.0}, page_content=&#39;&quot;As long as I don\&#39;t have to pretend to be your se rvant anymore,&quot; Mat muttered. &quot;As long as you don\&#39;t go \nmad and . . . . &quot; He let the words die with a twist of his mouth. \n&quot;Rand is not mad, Mat,&quot; Loial said. &quot;The Cairhienin w ould never have let him in here if he were not a \nlord. They are the ones who are mad.&quot; \n&quot;I\&#39;m not mad,&quot; Rand said harshly. &quot;Not yet. Hurin, show me this garden.&quot; \n&quot;This way, Lord Rand.&quot; \nThey went out into the night by a small door that Rand had to duck to get through; Loial was forced to \nbend over and hunch his shoulders. Th ere was enough light in yellow pools from the windows above for Rand \nto make out brick walks between square flower beds. The shadows of stables and other outbuildings bulked in \nthe darkness to either side. Occasional fragments of musi c drifted out, from the servants below or from those \nentertaining their masters above. \nHurin led them along the walks until even the dim gl ow failed and they made their way by moonlight&#39;),<br> Document(id=&#39;chunk_1295&#39;, metadata={&#39;author&#39;: &#39;Robert Jordan&#39;, &#39;creator&#39;: &#39;PyPDF&#39;, &#39;page&#39;: 239.0, &#39;page_label&#39;: &#39;240&#39;, &#39;producer&#39;: &#39;Python PDF Library - http://pybrary.net/pyPdf/&#39;, &#39;source&#39;: &#39;data/The Great Hunt.pdf&#39;, &#39;title&#39;: &#39;The Great Hunt&#39;, &#39;total_pages&#39;: 343.0}, page_content=&#39;&quot;There, Lord Rand,&quot; Hurin whispered, pointing. \nAhead, stone walls not much higher than Loial\&#39;s head  enclosed a square perhaps fifty paces on a side. \nRand could not be sure, in the shadows, but it looke d as if the gardens stretched on beyond the walls. He \nwondered why Barthanes had built a walled enclosure in th e middle of his garden. No roof showed above the \nwall. Why would they go in there and stay? \nLoial bent to put his mouth close to Rand\&#39;s ear. &quot;I told you this was all an Ogier grove, once. Rand, the \nWaygate is within that wall. I can feel it. &quot; \nRand heard Mat sigh despairingly. &quot;We can\&#39;t give up, Mat,&quot; he said. \n&quot;I\&#39;m not giving up. I just have enough brains not to want to travel the Ways again.&quot; \n&quot;We may have to,&quot; Rand told him.  &quot;Go find Ingtar and Verin. Get them alone somehow – I don\&#39;t care \nhow - and tell them I think Fain has taken the Horn through a Waygate. Just don\&#39;t let anyone else hear. And&#39;),<br> Document(id=&#39;chunk_1297&#39;, metadata={&#39;author&#39;: &#39;Robert Jordan&#39;, &#39;creator&#39;: &#39;PyPDF&#39;, &#39;page&#39;: 239.0, &#39;page_label&#39;: &#39;240&#39;, &#39;producer&#39;: &#39;Python PDF Library - http://pybrary.net/pyPdf/&#39;, &#39;source&#39;: &#39;data/The Great Hunt.pdf&#39;, &#39;title&#39;: &#39;The Great Hunt&#39;, &#39;total_pages&#39;: 343.0}, page_content=&#39;not believe Mat would betray him on purpose; there was that much of their friendship left, at least. &quot;Loial, boost \nme up where I can see over the wall. &quot; \n&quot;Rand, if the Darkfriends are still - &quot; \n&quot;They aren\&#39;t. Boost me up, Loial.&quot; \nThe three of them moved close to the wall, and Loial made a stirrup with his hands for Rand\&#39;s foot. The \nOgier straightened easily with the weight, lifting Rand\&#39;s head just high enough to see over the top of the wall.&#39;),<br> Document(id=&#39;chunk_1294&#39;, metadata={&#39;author&#39;: &#39;Robert Jordan&#39;, &#39;creator&#39;: &#39;PyPDF&#39;, &#39;page&#39;: 239.0, &#39;page_label&#39;: &#39;240&#39;, &#39;producer&#39;: &#39;Python PDF Library - http://pybrary.net/pyPdf/&#39;, &#39;source&#39;: &#39;data/The Great Hunt.pdf&#39;, &#39;title&#39;: &#39;The Great Hunt&#39;, &#39;total_pages&#39;: 343.0}, page_content=&#39;entertaining their masters above. \nHurin led them along the walks until even the dim gl ow failed and they made their way by moonlight \nalone, their boots crunching softly on the brick. Bushes that  would have been bright with flowers by daylight \nnow made strange humps in -the dark. Rand fingered his sword and did not let his eyes stay on any one spot too \nlong. A hundred Trollocs could be hiding around them unsee n. He knew Hurin would have smelled Trollocs if \nthey were there, but that did not help a great deal. If  Barthanes was a Darkfriend, then at least some of his \nservants and guards had to be, too, and Hurin could not always smell a Darkfriend. Darkfriends leaping out of \nthe night would not be much better than Trollocs. \n&quot;There, Lord Rand,&quot; Hurin whispered, pointing. \nAhead, stone walls not much higher than Loial\&#39;s head  enclosed a square perhaps fifty paces on a side.&#39;),<br> Document(id=&#39;chunk_1296&#39;, metadata={&#39;author&#39;: &#39;Robert Jordan&#39;, &#39;creator&#39;: &#39;PyPDF&#39;, &#39;page&#39;: 239.0, &#39;page_label&#39;: &#39;240&#39;, &#39;producer&#39;: &#39;Python PDF Library - http://pybrary.net/pyPdf/&#39;, &#39;source&#39;: &#39;data/The Great Hunt.pdf&#39;, &#39;title&#39;: &#39;The Great Hunt&#39;, &#39;total_pages&#39;: 343.0}, page_content=&#39;how - and tell them I think Fain has taken the Horn through a Waygate. Just don\&#39;t let anyone else hear. And \nremember to limp; you are supposed to have had a fall.&quot; It was a wonder to him that even Fain would risk the \nWays, but it seemed the only answer. They wouldn\&#39;t spend a day and a night just sitting in there, without a roof \nover their heads. \nMat swept a low bow, and his voice was heavy with sa rcasm. &quot;At once, my Lord. As my Lord wishes. \nShall I carry your banner, my Lord?&quot; He started back fo r the manor, his grumbles fa ding away. &quot;Now I have to \nlimp. Next it\&#39;ll be a broken neck, or . . . .&quot; \n&quot;He\&#39;s just worried about the dagger, Rand,&quot; Loial said. \n&quot;I know,&quot; Rand said. But how long before he tells somebody what I am, not even meaning to? He could \nnot believe Mat would betray him on purpose; there was that much of their friendship left, at least. &quot;Loial, boost \nme up where I can see over the wall. &quot; \n&quot;Rand, if the Darkfriends are still - &quot;&#39;),<br> Document(id=&#39;chunk_1292&#39;, metadata={&#39;author&#39;: &#39;Robert Jordan&#39;, &#39;creator&#39;: &#39;PyPDF&#39;, &#39;page&#39;: 239.0, &#39;page_label&#39;: &#39;240&#39;, &#39;producer&#39;: &#39;Python PDF Library - http://pybrary.net/pyPdf/&#39;, &#39;source&#39;: &#39;data/The Great Hunt.pdf&#39;, &#39;title&#39;: &#39;The Great Hunt&#39;, &#39;total_pages&#39;: 343.0}, page_content=&#39;Mat shook his head glumly. &quot;The only thing I\&#39;ve sensed is that somebody\&#39;s watching me, most of the \ntime. These people are as bad as Fades for sneaking ar ound. Burn me, I nearly jumped out of my skin when \nHurin told me he\&#39;d located the Darkfriends\&#39; trail. Rand, I can\&#39;t feel it at all, and I\&#39;ve been through this bloody \nbuilding from rafters to basement.&quot; \n&quot;That does not mean it isn\&#39;t here, Mat. I put it in th e chest with the Horn, remember. Maybe that keeps \nyou from feeling it. I don\&#39;t think Fain knows how to open it,  else he\&#39;d not have gone to the trouble of carrying \nthe weight when he fled Fal Dara. Even that much gold isn\&#39;t important beside the Horn of Valere. When we find \nthe Horn, we will find the dagger. You\&#39;ll see.&quot; \n&quot;As long as I don\&#39;t have to pretend to be your se rvant anymore,&quot; Mat muttered. &quot;As long as you don\&#39;t go \nmad and . . . . &quot; He let the words die with a twist of his mouth.&#39;)]</pre><h3>Weaviate</h3><p>Weaviate is an open-source vector database that can be self-hosted or used as a managed service. It offers strong schema validation, GraphQL API, and multi-tenancy.</p><p>To create a free Weaviate Cloud Sandbox, follow these steps:</p><p><a href="https://weaviate.io/developers/wcs/quickstart">Quickstart | Weaviate</a></p><ul><li>Visit the <a href="https://console.weaviate.cloud/">Weaviate Cloud Console</a>.</li></ul><blockquote>Click on “Sign Up” and provide your email address and password.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rcRECSXZB80MKk4Uv44fWw.png" /><figcaption>Click on “Sign Up” and provide your email address and password</figcaption></figure><ul><li>Confirm your email address through the verification email sent to you.</li><li>Log in to your new Weaviate Cloud account.</li><li>In the Weaviate Cloud Console, click on “Create cluster.”</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gJSUaGshFWmLLJpd0rSbpg.png" /><figcaption>click on “Create cluster”</figcaption></figure><blockquote>Select the “Sandbox” tab.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aKMlPVWo7B_-tGYddVX6zg.png" /><figcaption>Select the “Sandbox” tab</figcaption></figure><ul><li>Choose a name for your cluster.</li><li>Select a cloud region from the dropdown menu.</li><li>Click the “Create” button.</li></ul><p>The Sandbox cluster is free for 14 days, designed for learning and experimentation.</p><blockquote>Click connect to get <em>WEAVIATE_URL</em> and <em>WEAVIATE_API_KEY</em>.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Srkm0dMahfZQfPs31a3LRA.png" /><figcaption>Click Connect</figcaption></figure><p>To update your .env file with WEAVIATE_URL and WEAVIATE_API_KEY, reload the environment, and add the necessary dependencies to your Poetry environment.</p><pre>poetry add weaviate-client python-dotenv</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VBgj5GTrYeGj0upErBq5vg.png" /></figure><pre>import os<br>from dotenv import load_dotenv<br><br># Load environment variables<br>load_dotenv() <br><br># check WEAVIATE_URL is set<br>if os.getenv(&quot;WEAVIATE_URL&quot;) is None:<br>    print(&quot;WEAVIATE_URL is not set&quot;)<br>    exit(1)<br>else:<br>    print(&quot;WEAVIATE_URL is set&quot;)<br><br># check WEAVIATE_API_KEY is set<br>if os.getenv(&quot;WEAVIATE_API_KEY&quot;) is None:<br>    print(&quot;WEAVIATE_API_KEY is not set&quot;)<br>    exit(1)<br>else:<br>    print(&quot;WEAVIATE_API_KEY is set&quot;)<br><br>import weaviate<br>from weaviate.classes.init import Auth<br><br># Best practice: store your credentials in environment variables<br>weaviate_url = os.environ[&quot;WEAVIATE_URL&quot;]<br>weaviate_api_key = os.environ[&quot;WEAVIATE_API_KEY&quot;]<br><br># Connect to Weaviate Cloud<br>client = weaviate.connect_to_weaviate_cloud(<br>    cluster_url=weaviate_url,<br>    auth_credentials=Auth.api_key(weaviate_api_key),<br>)<br><br># Verify connection<br>print(client.is_ready())</pre><h4>Creating a Weaviate Vector Store</h4><pre>from langchain_openai import OpenAIEmbeddings<br>from langchain_weaviate.vectorstores import WeaviateVectorStore<br><br># Initialize embeddings<br>embeddings = OpenAIEmbeddings(model=&quot;text-embedding-3-large&quot;)<br><br># Create vector store from documents<br>db = WeaviateVectorStore.from_documents(<br>    chunks, <br>    embeddings, <br>    client=client, <br>    index_name=&quot;greatHunt&quot; # Optional index name<br>)<br><br># list all the collections<br>client.collections.list_all()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DDzT8HGNrsCZrTYE3SHeZw.png" /><figcaption>Check collections from Weaviate Cloud Console</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E3f1urh0_9ltcD2j6f85tg.png" /><figcaption>Explore the Schema of collection from Weaviate Cloud Console</figcaption></figure><h4>Querying Weaviate</h4><pre># List all collections<br>collections = client.collections.list_all()<br>print(collections)<br><br># Initialize with existing collection<br>weaviate = WeaviateVectorStore(<br>    client=client, <br>    index_name=&quot;greatHunt&quot;, <br>    text_key=&quot;text&quot;,<br>    embedding=embeddings  # Pass embeddings during initialization<br>)<br><br># Similarity search<br>results = weaviate.similarity_search(<br>    query=&quot;How is Rand’s heron-marked sword made?&quot;, <br>    k=10<br>)</pre><h4>Search with metadata filtering</h4><pre>from weaviate.classes.query import Filter<br><br># Corrected filter format<br>filters = Filter.by_property(&quot;page_label&quot;).equal(&quot;11&quot;)<br><br>results_filtered = weaviate.similarity_search_with_score(<br>    query=&quot;How is Rand&#39;s heron-marked sword made?&quot;,<br>    k=10,<br>    filters=filters<br>)</pre><h3>Milvus</h3><p>Milvus is an open-source vector database built specifically for similarity search and AI applications. It offers both self-hosted and cloud options with Zilliz Cloud, providing flexibility for different deployment needs.</p><p>For a detailed explanation of how to deploy a self-hosted <strong>Milvus Cluster</strong> on Kubernetes, refer to my Medium article below.</p><p><a href="https://medium.com/@wltsankalpa/building-a-scalable-rag-system-with-milvus-on-istio-service-mesh-indexing-the-wheel-of-time-d04f7e315cfd">Building a Scalable RAG System with Milvus on Istio Service Mesh: Indexing “The Wheel of Time”…</a></p><h4>Milvus managed Offering: Zilliz Cloud</h4><p>In this article, we’ll focus on <strong>Milvus managed cloud</strong> options using <strong>Zilliz Cloud</strong>.</p><p><a href="https://docs.zilliz.com/docs/free-trials">Try Zilliz Cloud For Free | Cloud | Zilliz Cloud Developer Hub</a></p><blockquote>Zilliz Cloud offers both free plan and free trial to help you evaluate or test the powerful Milvus vector database features and estimate costs of using Zilliz Cloud. To start, simply <a href="https://docs.zilliz.com/docs/register-with-zilliz-cloud">register an account</a> on Zilliz Cloud. No payment information is required.</blockquote><h4>Free plan<a href="https://docs.zilliz.com/docs/free-trials#free-plan">​</a></h4><blockquote>Zilliz Cloud offers a free plan that allows you to use Serverless cluster features at no cost. The free plan provides you with:</blockquote><blockquote>5 GB storage (Enough for 1M 768 dim vectors.)</blockquote><blockquote>2.5M vCUs per month (Total Read vCUs and Write vCUs per month)</blockquote><blockquote>Up to 5 collections</blockquote><blockquote>If you need more resources or want to access advanced features, please use the free trial of their Serverless and Dedicated plans.</blockquote><blockquote>Free trial</blockquote><blockquote>Zilliz Cloud offers free trial for three subscription plans: Serverless, Dedicated (Standard), and Dedicated (Enterprise). When you sign up for Zilliz Cloud, your organization will receive $100 in free credits added to your billing account. These credits expire after 30 days and can be used to explore the Serverless and Dedicated plans. Once the credits are used up or expire, the free trial ends.</blockquote><h4>Detailed Plan Comparison</h4><p><a href="https://docs.zilliz.com/docs/select-zilliz-cloud-service-plans">Detailed Plan Comparison | Cloud | Zilliz Cloud Developer Hub</a></p><blockquote>Zilliz Cloud categorizes its offerings into five distinct plans: Free, Serverless, Dedicated-Standard, Dedicated-Enterprise, and Bring Your Own Cloud (BYOC).</blockquote><h4>Create Cluster</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8GMJkoQ9fNW3f6NFmETOyw.png" /></figure><blockquote>Zilliz Cloud provides various cluster plan tiers to accommodate the distinct business needs of users. For guidance on selecting an appropriate cluster type check below documentation.</blockquote><p>After creating the cluster, retrieve the cluster’s <strong>Public Endpoint</strong> and set it as <strong>MILVUS_SERVERLESS_URL</strong>. .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NJiLv1XXjJax78pwv3UavA.png" /></figure><p>Then, click on <strong>User:Password</strong> to obtain the <strong>username</strong> and <strong>password</strong>. Set these as the environment variables <strong>MILVUS_SERVERLESS_USER</strong> and <strong>MILVUS_SERVERLESS_PASSWORD</strong>. Add them to the .env file and reload the environment variables.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mvco3hb07Ok9YC6nw9yY-Q.png" /></figure><p>Next, install the following Python dependencies for the Poetry environment:</p><pre>langchain-project-py3.11tharindu@Tharindus-MacBook-Pro langchain-project % poetry add langchain_milvus  <br>Using version ^0.1.8 for langchain-milvus<br><br>Updating dependencies<br>Resolving dependencies... (3.0s)<br><br>Package operations: 4 installs, 4 updates, 0 removals<br><br>  - Downgrading grpcio (1.70.0 -&gt; 1.67.1)<br>  - Downgrading grpcio-status (1.70.0 -&gt; 1.67.1)<br>  - Downgrading grpcio-health-checking (1.70.0 -&gt; 1.67.1)<br>  - Downgrading grpcio-tools (1.70.0 -&gt; 1.67.1)<br>  - Installing milvus-lite (2.4.11)<br>  - Installing ujson (5.10.0)<br>  - Installing pymilvus (2.5.5)<br>  - Installing langchain-milvus (0.1.8)<br><br>Writing lock file<br>langchain-project-py3.11tharindu@Tharindus-MacBook-Pro langchain-project % </pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NJiLv1XXjJax78pwv3UavA.png" /><figcaption>It can be noted that the concept of database is only available to Dedicated clusters. Serverless and Free clusters do not have databases.</figcaption></figure><blockquote>Zilliz Cloud introduces a database layer in between the clusters and collections, providing a more efficient way to manage and organize your data while supporting multi-tenancy. In Zilliz Cloud, a database serves as a logical unit for organizing and managing data. To enhance data security and achieve multi-tenancy, you can create multiple databases to logically isolate data for different applications or tenants. For example, you create a database to store the data of user A and another database for user B. The resources are structured in the following hierarchical order in Zilliz Cloud.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-1RCtdr__zNjnMm1-DvDvw.png" /></figure><p><a href="https://docs.zilliz.com/docs/database">Database | Cloud | Zilliz Cloud Developer Hub</a></p><h4>Connect to Milvus database</h4><pre>from langchain_milvus import Milvus  # Import the Milvus vector store from LangChain<br>from langchain_openai import OpenAIEmbeddings  # Import OpenAI&#39;s embedding model<br>import os  # For accessing environment variables<br><br># Initialize the OpenAI embeddings model for converting text to vectors<br>embeddings = OpenAIEmbeddings(model=&quot;text-embedding-3-large&quot;)<br><br># Define index parameters for Milvus - these control how vectors are stored and searched<br>index_params = {<br>    &quot;metric_type&quot;: &quot;COSINE&quot;,  # Use cosine similarity for comparing vectors<br>    &quot;index_type&quot;: &quot;HNSW&quot;,     # Hierarchical Navigable Small World graph index type - good for high recall<br>    &quot;params&quot;: {<br>        &quot;M&quot;: 48,              # Number of bi-directional links in the HNSW graph - higher means better recall but more memory<br>        &quot;ef_construction&quot;: 400,  # Search scope during index building - higher means better index quality but slower build<br>        &quot;ef_search&quot;: 1000     # Search scope during searching - higher means better recall but slower queries<br>    }<br>}<br><br># Connect to Milvus database using credentials from environment variables<br>vector_store = Milvus(<br>    embedding_function=embeddings,  # Function to create embeddings from text<br>    connection_args={<br>        &quot;uri&quot;: os.getenv(&quot;MILVUS_SERVERLESS_URL&quot;),  # Serverless Milvus endpoint<br>        &quot;user&quot;: os.getenv(&quot;MILVUS_SERVERLESS_USER&quot;),  # Authentication username<br>        &quot;password&quot;: os.getenv(&quot;MILVUS_SERVERLESS_PASSWORD&quot;),  # Authentication password<br>        &quot;db_name&quot;: &quot;wheel_of_time_db&quot;  # Target database name<br>    },<br>    collection_name=&quot;wheeloftime_hnsw_cosine&quot;,  # Name of the vector collection in Milvus<br>    drop_old=False,   # Keep existing collection if it exists (set to True to recreate)<br>    auto_id=True,     # Let Milvus auto-generate document IDs<br>    index_params=index_params  # Apply the index configuration defined above<br>)<br></pre><pre>Added batch 1/19 (100/1838 documents)<br>Added batch 2/19 (200/1838 documents)<br>Added batch 3/19 (300/1838 documents)<br>Added batch 4/19 (400/1838 documents)<br>Added batch 5/19 (500/1838 documents)<br>Added batch 6/19 (600/1838 documents)<br>Added batch 7/19 (700/1838 documents)<br>Added batch 8/19 (800/1838 documents)<br>Added batch 9/19 (900/1838 documents)<br>Added batch 10/19 (1000/1838 documents)<br>Added batch 11/19 (1100/1838 documents)<br>Added batch 12/19 (1200/1838 documents)<br>Added batch 13/19 (1300/1838 documents)<br>Added batch 14/19 (1400/1838 documents)<br>Added batch 15/19 (1500/1838 documents)<br>Added batch 16/19 (1600/1838 documents)<br>Added batch 17/19 (1700/1838 documents)<br>Added batch 18/19 (1800/1838 documents)<br>Added batch 19/19 (1838/1838 documents)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TO5y0dfheEF4KTndg1ag4w.png" /><figcaption>Collection Schema details and no of Loaded Entities</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5TA0WcnmTlBor4ovPO7sPw.png" /><figcaption>Data Preview</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SSgIKy4kMA2wi4uWz4p32Q.png" /><figcaption>Free Clusters Usage Metrics (Keep a eye on 2.5M vCUs per month)</figcaption></figure><h4>Insert documents in batches</h4><pre># Helper function to insert documents in batches for better performance<br>def batch_add_documents(vector_store, documents, batch_size=100):<br>    &quot;&quot;&quot;Add documents to Milvus in batches&quot;&quot;&quot;<br>    total = len(documents)  # Total number of documents to process<br>    <br>    # Split documents into batches of specified size<br>    batches = [documents[i:i + batch_size] for i in range(0, total, batch_size)]<br>    <br>    success_count = 0  # Counter for successfully inserted documents<br>    for i, batch in enumerate(batches):<br>        try:<br>            # Insert the current batch of documents<br>            vector_store.add_documents(batch)<br>            success_count += len(batch)<br>            # Print progress update<br>            print(f&quot;Added batch {i+1}/{len(batches)} ({success_count}/{total} documents)&quot;)<br>        except Exception as e:<br>            # Handle and report any errors during insertion<br>            print(f&quot;Error adding batch {i+1}: {e}&quot;)<br>    <br>    # Return success status and statistics<br>    return success_count == total, {&quot;total&quot;: total, &quot;success&quot;: success_count}<br><br># Execute the batch insertion with the documents named &#39;chunks&#39;<br># Using a batch size of 100 documents per insertion<br>success, stats = batch_add_documents(vector_store, chunks, batch_size=100)</pre><h4>Querying with Milvus</h4><pre># Connect to Milvus for querying (without drop_old=True to preserve data)<br>vector_store = Milvus(<br>    embedding_function=embeddings,<br>    connection_args={<br>        &quot;uri&quot;: os.getenv(&quot;MILVUS_SERVERLESS_URL&quot;),<br>        &quot;user&quot;: os.getenv(&quot;MILVUS_SERVERLESS_USER&quot;),<br>        &quot;password&quot;: os.getenv(&quot;MILVUS_SERVERLESS_PASSWORD&quot;)<br>    },<br>    collection_name=&quot;wheeloftime_great_hunt_hsnw_cosine&quot;<br>)<br><br># Similarity search with scores<br>results = vector_store.similarity_search_with_score(&quot;How is Rand&#39;s heron-marked sword made?&quot;, k=10)<br><br><br># Function to print Milvus search results in a readable format<br>def print_results(results):<br>    print(&quot;\n===== SEARCH RESULTS =====\n&quot;)<br>    <br>    for i, (doc, score) in enumerate(results):<br>        # Convert score to percentage<br>        score_pct = score * 100<br>        <br>        # Get page number from metadata<br>        page = doc.metadata.get(&#39;page_label&#39;, &#39;Unknown&#39;)<br>        <br>        # Get a short preview of content (first 100 characters)<br>        content_preview = doc.page_content.replace(&#39;\n&#39;, &#39; &#39;)[:100] + &#39;...&#39;<br>        <br>        print(f&quot;RESULT #{i+1} - SCORE: {score_pct:.2f}%&quot;)<br>        print(f&quot;PAGE: {page}&quot;)<br>        print(f&quot;SOURCE: {doc.metadata.get(&#39;source&#39;, &#39;Unknown&#39;)}&quot;)<br>        print(f&quot;PREVIEW: {content_preview}&quot;)<br>        print(&quot;-&quot; * 50)<br><br><br># Use the function to print your results<br>print_results(results)</pre><pre>===== SEARCH RESULTS =====<br><br>RESULT #1 - SCORE: 65.85%<br>PAGE: 12<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: weapon for one man to kill another. Every Aes Sedai swor e it, and every woman of them since has kep...<br>--------------------------------------------------<br>RESULT #2 - SCORE: 62.00%<br>PAGE: 12<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: blademaster and be awarded a heron-mark blade, but even so, not enough Aes Sedai blades remain for m...<br>--------------------------------------------------<br>RESULT #3 - SCORE: 60.10%<br>PAGE: 12<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: &quot;You could sell it,&quot; Lan said carefu lly. &quot;That blade is rare even among heron-mark swords. It would...<br>--------------------------------------------------<br>RESULT #4 - SCORE: 57.10%<br>PAGE: 321<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: Turak glided toward him on silent feet. Blade rang on blade like hammer on anvil.  From the first it...<br>--------------------------------------------------<br>RESULT #5 - SCORE: 56.64%<br>PAGE: 198<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: sword makes five. Hilt, scabbard, and blade.&quot; He turn ed his hand down on the table, hiding the bran...<br>--------------------------------------------------<br>RESULT #6 - SCORE: 55.62%<br>PAGE: 321<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: did you find that blade, boy? Or do they here truly aw ard the heron to those no more skilled than y...<br>--------------------------------------------------<br>RESULT #7 - SCORE: 53.63%<br>PAGE: 336<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: garden behind, and there he was.  Rand lay sprawled on his back under an oak, face pale  and eyes cl...<br>--------------------------------------------------<br>RESULT #8 - SCORE: 53.52%<br>PAGE: 321<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: and his sword existed, or the sounds of fighting, fading now  from the rooms to either side out into...<br>--------------------------------------------------<br>RESULT #9 - SCORE: 53.17%<br>PAGE: 136<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: She was studying his sword, he saw. He readied the wo rds in his head. No, he was not a blademaster,...<br>--------------------------------------------------<br>RESULT #10 - SCORE: 52.51%<br>PAGE: 11<br>SOURCE: data/The Great Hunt.pdf<br>PREVIEW: sheepherder? A month since you said you were going, and I thought you&#39;d be three weeks gone by now.&quot;...<br>--------------------------------------------------</pre><h4>Define metadata filter parameters</h4><pre># Define filter parameters<br>source = &quot;data/The Great Hunt.pdf&quot;  # Replace with your book name variable<br>page_range = (5, 15)  # Replace with your chapter range variable (start, end)<br>additional_filters = None  # Any other filters you might want to add<br><br># Build the filter expression<br>filters = []<br>if source:<br>    filters.append(f&quot;source == &#39;{source}&#39;&quot;)<br>if page_range:<br>    start, end = page_range<br>    filters.append(f&quot;page &gt;= {start} and page &lt;= {end}&quot;)<br>        <br>combined_filter = &quot; and &quot;.join(filters) if filters else None<br><br><br># Similarity search with scores and filter<br>filter_results = vector_store.similarity_search_with_score(<br>    &quot;How is Rand&#39;s heron-marked sword made?&quot;, <br>    k=10,<br>    filter=combined_filter<br>)<br><br># Use the function to print your results<br>print_results(filter_results)</pre><h3>Chromadb</h3><p>Chroma is the open-source AI application database. Chroma makes it easy to build LLM apps by making knowledge, facts, and skills pluggable for LLMs.</p><pre>langchain-project-py3.11tharindu@Tharindus-MacBook-Pro langchain-project % poetry add chromadb<br>Using version ^0.6.3 for chromadb<br><br>Updating dependencies<br>Resolving dependencies... (11.3s)<br><br>Package operations: 50 installs, 0 updates, 0 removals<br><br>  - Installing zipp (3.21.0)<br>  - Installing deprecated (1.2.18)<br>  - Installing importlib-metadata (8.6.1)<br>  - Installing opentelemetry-api (1.31.0)<br>  - Installing mdurl (0.1.2)<br>  - Installing opentelemetry-semantic-conventions (0.52b0)<br>  - Installing asgiref (3.8.1)<br>  - Installing filelock (3.18.0)<br>  - Installing fsspec (2025.3.0)<br>  - Installing humanfriendly (10.0)<br>  - Installing pypika (0.48.9)<br>  - Installing tokenizers (0.21.1)<br>  - Installing typer (0.15.2)<br>  - Installing uvicorn (0.34.0)<br>  - Installing chromadb (0.6.3)<br><br>Writing lock file<br>langchain-project-py3.11tharindu@Tharindus-MacBook-Pro langchain-project %</pre><h4>Store in Chroma</h4><pre>import os<br>from langchain.text_splitter import RecursiveCharacterTextSplitter<br>from langchain.document_loaders import PyPDFLoader<br>from langchain_openai import OpenAIEmbeddings<br>from langchain.vectorstores import Chroma<br>from langchain.chains import RetrievalQA<br>from langchain.llms import OpenAI<br><br><br># 1. Load your document<br>loader = PyPDFLoader(&quot;data/The Great Hunt.pdf&quot;)<br>document = loader.load()<br>print(f&quot;Loaded document with {len(document)} pages&quot;)<br><br># 2. Split into chunks<br>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,<br>    chunk_overlap=200<br>)<br>chunks = text_splitter.split_documents(document)<br>print(f&quot;Split into {len(chunks)} chunks&quot;)<br><br># 3. Create embeddings<br>embeddings = OpenAIEmbeddings()<br><br># 4. Store in Chroma<br># Option 1: Create a new Chroma database<br>db = Chroma.from_documents(<br>    documents=chunks,<br>    embedding=embeddings,<br>    persist_directory=&quot;./data/chroma_db&quot;  # Where to store the database<br>)<br><br><br>print(&quot;Vector database created and persisted to disk&quot;)<br><br># Option 2: If you want to load an existing database later<br># loaded_db = Chroma(<br>#     persist_directory=&quot;./chroma_db&quot;,<br>#     embedding_function=embeddings<br># )<br></pre><h4>Create a QA chain using retrieved documents</h4><pre># 5. Simple query example<br>query = &quot;Who is the Amyrlin Seat and what happens when she arrives?&quot;<br>docs = db.similarity_search(query, k=3)  # Return 3 most relevant chunks<br>print(f&quot;\nRetrieved {len(docs)} relevant document chunks for query: &#39;{query}&#39;&quot;)<br>for i, doc in enumerate(docs):<br>    print(f&quot;\nResult {i+1}:&quot;)<br>    print(f&quot;Source: {doc.metadata.get(&#39;source&#39;, &#39;Unknown&#39;)}, Page: {doc.metadata.get(&#39;page&#39;, &#39;Unknown&#39;)}&quot;)<br>    print(f&quot;Content preview: {doc.page_content[:200]}...&quot;)<br><br># 6. Advanced: Create a QA chain using retrieved documents<br>retriever = db.as_retriever(search_kwargs={&quot;k&quot;: 4})<br>qa_chain = RetrievalQA.from_chain_type(<br>    llm=OpenAI(),<br>    chain_type=&quot;stuff&quot;,<br>    retriever=retriever<br>)<br><br># Ask a question<br>answer = qa_chain.run(query)<br>print(f&quot;\nAnswer: {answer}&quot;)<br><br># 7. Metadata filtering example (if your documents have metadata)<br># Example: Find chunks from a specific page range<br>page_filter = {<br>    &quot;$and&quot;: [<br>        {&quot;page&quot;: {&quot;$gte&quot;: 1}},<br>        {&quot;page&quot;: {&quot;$lte&quot;: 5}}<br>    ]<br>}<br>filtered_docs = db.similarity_search(<br>    &quot;What happens in the prologue?&quot;,<br>    k=2,<br>    filter=page_filter<br>)<br>print(&quot;\nFiltered search results (pages 1-5 only):&quot;)<br>for i, doc in enumerate(filtered_docs):<br>    print(f&quot;\nFiltered Result {i+1}:&quot;)<br>    print(f&quot;Source: {doc.metadata.get(&#39;source&#39;, &#39;Unknown&#39;)}, Page: {doc.metadata.get(&#39;page&#39;, &#39;Unknown&#39;)}&quot;)<br>    print(f&quot;Content preview: {doc.page_content[:150]}...&quot;)</pre><p>After exploring the various vector database options available in the LangChain ecosystem, we now have a solid foundation for implementing the Retrieval-Augmented Generation (RAG) system using LangChain components and let’s take a look at full example.</p><h3>Complete End-to-End Direct Component Integration Example</h3><p>Here’s a comprehensive example that demonstrates how to build a question-answering system using direct LangChain component integration:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cLKWgU2h-vW5RrvX83ox_g.png" /><figcaption>RAG system with direct LangChain component integration</figcaption></figure><p>This sequence diagram illustrates the step-by-step process of how a Retrieval Augmented Generation (RAG) system works when implemented with direct component integration in LangChain. It’s an excellent visual representation of the code example above.</p><pre>import os<br>from dotenv import load_dotenv<br>from langchain.document_loaders import PyPDFLoader<br>from langchain.text_splitter import RecursiveCharacterTextSplitter<br>from langchain_openai import OpenAIEmbeddings, ChatOpenAI<br>from langchain_community.vectorstores import FAISS<br>from langchain_core.prompts import ChatPromptTemplate<br><br># Load environment variables for API keys<br>load_dotenv()<br><br># 1. Document Loading - Load PDF document<br>print(&quot;Step 1: Loading document...&quot;)<br>loader = PyPDFLoader(&quot;./data/The Great Hunt.pdf&quot;)<br>documents = loader.load()<br>print(f&quot;Loaded {len(documents)} pages&quot;)<br><br># 2. Text Splitting - Split into manageable chunks <br>print(&quot;\nStep 2: Splitting document into chunks...&quot;)<br>text_splitter = RecursiveCharacterTextSplitter(<br>    chunk_size=1000,<br>    chunk_overlap=200,<br>    separators=[&quot;\n\n&quot;, &quot;\n&quot;, &quot; &quot;, &quot;&quot;]<br>)<br>chunks = text_splitter.split_documents(documents)<br>print(f&quot;Created {len(chunks)} chunks&quot;)<br><br># 3. Embedding &amp; Vector Storage - Create embeddings and store in FAISS<br>print(&quot;\nStep 3: Creating embeddings and storing in vector database...&quot;)<br>embeddings = OpenAIEmbeddings(model=&quot;text-embedding-3-large&quot;)<br>vector_store = FAISS.from_documents(chunks, embeddings)<br>print(&quot;Vector store created successfully&quot;)<br><br># 4. LLM Initialization - Setup the language model<br>print(&quot;\nStep 4: Initializing language model...&quot;)<br>llm = ChatOpenAI(model=&quot;gpt-3.5-turbo&quot;, temperature=0)<br>print(&quot;LLM initialized&quot;)<br><br># 5. Query Processing Function<br>def process_query(user_query, num_results=4):<br>    print(f&quot;\nProcessing query: &#39;{user_query}&#39;&quot;)<br>    <br>    # 5.1 Retrieve relevant chunks from vector store<br>    print(&quot;Retrieving relevant document chunks...&quot;)<br>    retrieved_docs = vector_store.similarity_search(user_query, k=num_results)<br>    <br>    # Create context from retrieved documents<br>    context = &quot;\n\n&quot;.join([doc.page_content for doc in retrieved_docs])<br>    <br>    # 5.2 Create prompt with retrieved context<br>    print(&quot;Creating prompt with context...&quot;)<br>    prompt = ChatPromptTemplate.from_template(&quot;&quot;&quot;<br>    You are a helpful assistant that provides accurate information based on the provided context.<br>    If you don&#39;t know the answer based on the context, say that you don&#39;t know.<br>    <br>    Context:<br>    {context}<br>    <br>    Question: {question}<br>    <br>    Answer:<br>    &quot;&quot;&quot;)<br>    <br>    # 5.3 Format prompt with context and query<br>    formatted_prompt = prompt.format(context=context, question=user_query)<br>    <br>    # 5.4 Send to LLM for final answer<br>    print(&quot;Generating answer from LLM...&quot;)<br>    response = llm.invoke(formatted_prompt)<br>    <br>    return {<br>        &quot;query&quot;: user_query,<br>        &quot;answer&quot;: response.content,<br>        &quot;source_documents&quot;: retrieved_docs<br>    }<br><br># 6. Execute Example Query<br>query = &quot;What are the main themes of the book?&quot;<br>result = process_query(query)<br><br># 7. Display Results<br>print(&quot;\n&quot; + &quot;=&quot; * 50)<br>print(&quot;QUERY:&quot;, result[&quot;query&quot;])<br>print(&quot;-&quot; * 50)<br>print(&quot;ANSWER:&quot;, result[&quot;answer&quot;])<br>print(&quot;-&quot; * 50)<br>print(&quot;SOURCES:&quot;)<br>for i, doc in enumerate(result[&quot;source_documents&quot;]):<br>    print(f&quot;Source {i+1}: {doc.metadata.get(&#39;source&#39;, &#39;Unknown&#39;)}, Page {doc.metadata.get(&#39;page&#39;, &#39;Unknown&#39;)}&quot;)<br>    print(f&quot;Preview: {doc.page_content[:100]}...\n&quot;)</pre><p>This example demonstrates how to manually integrate LangChain components to build a Retrieval-Augmented Generation (RAG) system. By coding each step explicitly, you gain complete visibility into the workflow and can easily customize any part of the process.</p><p>In the next section, we’ll explore how LangSmith can help monitor and debug applications built using this approach, as well as those using more advanced techniques like LCEL and LangGraph.</p><h3>LangSmith Integration</h3><p>LangSmith is LangChain’s development, debugging, and monitoring platform. It helps track and analyze your LLM application’s performance.</p><h4>LangSmith Environment Variables</h4><p>You’ve set up these environment variables:</p><ol><li>LANGSMITH_API_KEY: Your LangSmith authentication key</li><li>LANGSMITH_ENDPOINT: The API endpoint (defaults to <a href="https://api.smith.langchain.com)/">https://api.smith.langchain.com)</a></li><li>LANGSMITH_PROJECT: The project name for grouping related runs</li><li>LANGSMITH_TRACING: When set to &quot;true&quot;, enables automatic tracing of all LangChain operations</li></ol><p>When these variables are set, all your LangChain operations are automatically traced and sent to your LangSmith dashboard.</p><h4>Setting Up LangSmith</h4><p>To set up LangSmith:</p><ol><li><strong>Create an account</strong>: Visit <a href="https://smith.langchain.com/">smith.langchain.com</a> and sign up (free tier available)</li><li><strong>Get your API key</strong>: After signing in, navigate to the Settings page to find your API key</li><li><strong>Create a project</strong>: Projects help organize your traces by application or experiment</li><li><strong>Set environment variables</strong>: Use the variables you mentioned to connect your code</li><li><strong>Run your code</strong>: All LangChain operations will now be traced automatically</li></ol><h4>Viewing Traces in LangSmith</h4><p>After running your code with LangSmith enabled, you can:</p><ol><li>Go to your LangSmith dashboard</li><li>Navigate to the “Traces” section</li><li>Filter by your project name</li><li>View detailed information about each run, including:</li></ol><ul><li>Input/output for each step</li><li>Tokens used and latency</li><li>Chain and prompt structures</li><li>Error information (if any)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6CFLjeGMjSN_X5EkF45VpA.png" /><figcaption>LangSmith dashboard Traces</figcaption></figure><p>LangSmith is particularly valuable when debugging complex chains and agents, as it visualizes the entire execution flow and makes it easier to identify where issues might occur.</p><h3>Conclusion</h3><blockquote>This article provides a foundational yet comprehensive exploration of the LangChain ecosystem, guiding you step-by-step through the direct integration of its core components. As the introductory article in a series, it sets the stage for deeper dives into more sophisticated development strategies. In upcoming articles, we’ll explore LangChain Expression Language (LCEL) and demonstrate how LCEL and Runnables enable a more seamless and efficient integration of LangChain components compared to manual setups. Additionally, we’ll tackle advanced use cases, illustrating how to create robust, agent-based workflows using LangGraph. Stay tuned as we continue demystifying LangChain, empowering you to build smarter, more capable LLM-powered applications.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6c1fbff064ea" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Scalable RAG System with Milvus on Istio Service Mesh: Indexing “The Wheel of Time”…]]></title>
            <link>https://medium.com/@wltsankalpa/building-a-scalable-rag-system-with-milvus-on-istio-service-mesh-indexing-the-wheel-of-time-d04f7e315cfd?source=rss-4df012e8ed5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/d04f7e315cfd</guid>
            <dc:creator><![CDATA[Wltsankalpa]]></dc:creator>
            <pubDate>Mon, 03 Feb 2025 08:40:05 GMT</pubDate>
            <atom:updated>2025-02-03T08:40:05.457Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building a Scalable RAG System with Milvus on Istio Service Mesh: Indexing <strong>“The Wheel of Time”</strong> Series</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GjHmeKKITqZJbM7LKvyLUA.png" /></figure><p>In today’s AI landscape, integrating generative models with retrieval systems is transforming the way we interact with complex data. In this article, I share my experience in building a scalable Retrieval-Augmented Generation (RAG) system that leverages cutting-edge vector databases and cloud-native infrastructure. By using Robert Jordan’s expansive <em>The Wheel of Time</em> series as my testbed, I demonstrate how to process, embed, and semantically search thousands of pages of intricate text. My approach combines <strong>Milvus</strong> for vector storage, <strong>Kubernetes</strong> for orchestration, <strong>Istio</strong> for secure service communication, and a robust observability stack with <strong>Grafana</strong> and <strong>Prometheus</strong> — all to ensure the system remains both high-performing and reliable.</p><h3>Introduction</h3><p>Retrieval-Augmented Generation (RAG) systems have redefined AI applications by merging the capabilities of large language models with precise, context-aware search techniques. In this article, I detail my experience from concept to deployment, showcasing how I designed a RAG system that is both scalable and efficient enough to handle a vast corpus of nuanced text.</p><p>I chose <em>The Wheel of Time</em> series as my dataset because its rich, multilayered narrative presents unique challenges and opportunities for testing semantic search and vector indexing techniques. Throughout this article, I explain:</p><ul><li><strong>Why RAG Systems Matter:</strong> I outline the benefits of combining generative AI with retrieval strategies to produce contextually relevant responses.</li><li><strong>The Rationale Behind the Dataset:</strong> I discuss why an expansive series like <em>The Wheel of Time</em> serves as the perfect challenge for my system.</li><li><strong>The Technology Stack:</strong> I walk you through my choice of technologies — Milvus, Kubernetes, Istio, and observability tools like Grafana and Prometheus — and explain how each component plays a crucial role.</li><li><strong>Step-by-Step Implementation:</strong> I provide detailed instructions, from setting up my development environment to deploying a full-scale solution on Kubernetes.</li></ul><p>This comprehensive guide is intended for developers and AI enthusiasts who are interested in advanced AI techniques and modern cloud-native architectures, and who want to see how these systems come together in a real-world scenario.</p><h3>Why The Wheel of Time?</h3><p>I specifically selected <em>The Wheel of Time</em> series due to its sheer volume and complexity. It consists of <strong>14 main books plus a prequel</strong>, with each book averaging around <strong>400–600 pages</strong>. This results in a total of <strong>more than 10,000 pages</strong> of content to process. The scale and depth of this series make it an ideal dataset to evaluate the efficiency of a RAG system.</p><pre>tharindu@192 epub % pwd<br>/Users/tharindu/git/rag-pdf-qa-backend/data/epub<br>tharindu@192 epub %<br>tharindu@192 epub %<br>tharindu@192 epub % ls -lht<br>total 48544<br>-rw-rw-r--@ 1 tharindu  staff   642K  2 Oct 23:02 00. New Spring.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.5M  2 Oct 23:02 01. The Eye of the World.epub<br>-rw-rw-r--@ 1 tharindu  staff   988K  2 Oct 23:02 02. The Great Hunt.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.4M  2 Oct 23:02 03. The Dragon Reborn.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.9M  2 Oct 23:02 04. The Shadow Rising.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.4M  2 Oct 23:02 05. The Fires of Heaven.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.3M  2 Oct 23:02 06. Lord of Chaos.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.1M  2 Oct 23:02 07. A Crown of Swords.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.1M  2 Oct 23:02 08. The Path of Daggers.epub<br>-rw-rw-r--@ 1 tharindu  staff   994K  2 Oct 23:02 09. Winter_s Heart.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.2M  2 Oct 23:02 10. Crossroads of Twilight.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.3M  2 Oct 23:02 11. Knife of Dreams.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.1M  2 Oct 23:02 12. The Gathering Storm.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.6M  2 Oct 23:02 13. Towers of Midnight.epub<br>-rw-rw-r--@ 1 tharindu  staff   1.5M  2 Oct 23:02 14. A Memory of Light.epub<br>-rw-rw-r--@ 1 tharindu  staff   4.8M  2 Oct 23:02 The Wheel of Time Companion _ The People, Places and History of the Bestselling Series.epubTechnology Stack</pre><p>Beyond just its volume, <em>The Wheel of Time</em> is one of the most famous and influential fantasy series worldwide, known for its <strong>rich world-building</strong>, <strong>complex character development</strong>, and <strong>deeply interwoven lore</strong>. Robert Jordan crafted an expansive universe filled with diverse cultures, political intrigue, and a complex magic system that offers countless opportunities for semantic relationships between entities.</p><p>This series is an ideal test case for a <strong>RAG-based</strong> use case because it allows us to evaluate how well the system <strong>understands semantic relationships</strong> beyond simple keyword matching. Unlike traditional <strong>mathematical similarity</strong> or <strong>keyword-based searching</strong>, an effective RAG system must grasp nuanced contextual meanings — such as how characters, places, and events interconnect across thousands of pages.</p><p>Additionally, having personally read most of the books, I have an in-depth understanding of the story, which allows me to ask <strong>precise queries</strong> and effectively evaluate the system’s <strong>retrieval accuracy</strong>. My familiarity with the narrative, characters, and terminology gives me an advantage in <strong>fine-tuning the search pipeline</strong> for better performance and relevance.</p><h3>Technology Stack</h3><p>This project leverages a <strong>high-performance tech stack</strong>, including:</p><ul><li><strong>Milvus</strong>: An open-source vector database optimized for large-scale similarity search. Well-suited for microservice architecture and cloud-native stacks.</li><li><strong>LangChain Ecosystem</strong>: A comprehensive ecosystem for text processing, embedding, retrieval, agent-based systems, and RAG pipeline development and monitoring.</li><li><strong>OpenAI Embeddings</strong>: High-quality embeddings that enable semantic search, ensuring accurate retrieval of contextually relevant information rather than simple keyword matches. Essential for enhancing the precision of the RAG system.</li><li><strong>Kubernetes + Helm</strong>: Provides a scalable and resilient cloud-native deployment environment, ensuring efficient management, scaling, and orchestration of the entire pipeline. Helm simplifies deployment and configuration.</li><li><strong>Istio Service Mesh</strong>: Ensures secure, controlled, and observable service-to-service communication, optimizing API traffic flow and improving reliability, security, and fault tolerance in the system.</li><li><strong>Grafana &amp; Prometheus</strong>: Enables real-time monitoring and observability, allowing detailed tracking of query performance, system health, and workload metrics to ensure optimal efficiency of the RAG system.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Y-WNNE2JDmpesMb7-11sZg.png" /></figure><p>This diagram represents the <strong>end-to-end architecture</strong> of my Retrieval-Augmented Generation (RAG) system, focusing on <strong>data processing, vector storage, search, and observability</strong>. Below is a breakdown of each component in the system and its role in making the <strong>semantic search</strong> effective.</p><h3>Local Development Environment Setup</h3><p>Let’s establish a robust local development environment that supports building and testing our RAG system. The setup varies slightly by operating system.</p><h4>Operating System Options</h4><p><strong>Linux (Ubuntu 20.04 or later)</strong></p><ul><li>Native support for all required tools</li><li>Direct installation of Docker, Pyenv, and Poetry</li><li>Seamless Minikube integration</li></ul><p><strong>Windows 10/11</strong></p><ul><li>Recommended: Windows Subsystem for Linux 2 (WSL2) with Ubuntu</li><li>VSCode with Remote — WSL extension for seamless development</li><li>Two Docker options:</li><li>Recommended: Docker within WSL2</li><li>Docker Desktop (found some issues with WSL2 integration)</li></ul><p><strong>macOS</strong></p><ul><li>Intel Macs: Similar to Linux setup</li><li>Apple Silicon (M1/M2):</li><li>Requires ARM64-compatible versions of tools</li><li>Special considerations for TensorFlow and other ML libraries</li></ul><h4>Core Components</h4><p>Every development environment should include:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fhn5GAinCGkDf11FsZ4OCg.png" /></figure><h4>Environment Overview</h4><p><strong>Host System Options</strong></p><ul><li>Windows 10/11 with WSL2 Ubuntu</li><li>Native Ubuntu 20.04+</li><li>macOS (Intel/M1/M2)</li></ul><p><strong>Development Tools</strong></p><ul><li>VSCode + Extensions</li><li>Pyenv (Python 3.11)</li><li>Poetry</li><li>Docker Engine</li></ul><p><strong>Local Kubernetes Stack</strong></p><ul><li>Minikube with Docker driver</li><li>Core Services:</li><li>Keycloak for authentication/authorization</li><li>Istio service mesh for traffic management and observability</li></ul><p>This setup provides a consistent development experience across different platforms while maintaining the core infrastructure needed for RAG system development and testing.</p><h4>Setup Process (For Ubuntu 20.04 and WSL2 Ubuntu)</h4><pre>####################################<br># Initial System Setup<br><br># Update system packages<br>sudo apt-get update &amp;&amp; sudo apt-get upgrade -y<br><br># Install basic development tools<br>sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \<br>libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \<br>libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev<br><br># Install Docker<br>curl -fsSL https://get.docker.com -o get-docker.sh<br>sudo sh get-docker.sh<br><br># Add your user to docker group<br>sudo usermod -aG docker $USER<br>newgrp docker<br><br><br>####################################<br># Setting Up Pyenv<br><br># Install Pyenv<br>curl https://pyenv.run | bash<br><br># Add Pyenv to your shell configuration<br>echo &#39;export PYENV_ROOT=&quot;$HOME/.pyenv&quot;&#39; &gt;&gt; ~/.bashrc<br>echo &#39;command -v pyenv &gt;/dev/null || export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;&#39; &gt;&gt; ~/.bashrc<br>echo &#39;eval &quot;$(pyenv init -)&quot;&#39; &gt;&gt; ~/.bashrc<br><br># Reload shell configuration<br>source ~/.bashrc<br><br><br># List all available Python versions<br>pyenv install --list<br><br># Install Python 3.11<br>pyenv install 3.11.0<br><br># Set Python 3.11 as the local version for test project directory<br>mkdir test-project-dir<br>cd test-project-dir<br>pyenv local 3.11.0<br><br># Verify your active Python version<br>python --version<br><br><br>####################################<br># Install Poetry<br><br># Install Poetry<br>curl -sSL https://install.python-poetry.org | python3 -<br><br># Add Poetry to PATH<br>echo &#39;export PATH=&quot;$HOME/.local/bin:$PATH&quot;&#39; &gt;&gt; ~/.bashrc<br>source ~/.bashrc<br><br># Configure Poetry to create virtual environments in the project directory<br>poetry config virtualenvs.in-project true<br><br>####################################<br># Install kubectl<br>curl -LO &quot;https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl&quot;<br>sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl<br><br><br># Check Python and Poetry<br>python --version<br>poetry --version<br><br># Check Docker<br>docker --version<br>docker run hello-world</pre><h4>Project Setup</h4><p>You will need a openAI API token for this step. Now let’s set up our project structure and install dependencies:</p><pre># Git clone the project repo<br>git clone https://github.com/VLTSankalpa/wheel-of-time-rag.git<br><br># Initialize project directory<br>cd wheel-of-time-rag<br><br>tharindu@192 wheel-of-time-rag % tree<br>.<br>├── data<br>│   ├── epub<br>│   │   ├── 00. New Spring.epub<br>│   │   ├── 01. The Eye of the World.epub<br>│   │   ├── 02. The Great Hunt.epub<br>│   │   ├── 03. The Dragon Reborn.epub<br>│   │   ├── 04. The Shadow Rising.epub<br>│   │   ├── 05. The Fires of Heaven.epub<br>│   │   ├── 06. Lord of Chaos.epub<br>│   │   ├── 07. A Crown of Swords.epub<br>│   │   ├── 08. The Path of Daggers.epub<br>│   │   ├── 09. Winter_s Heart.epub<br>│   │   ├── 10. Crossroads of Twilight.epub<br>│   │   ├── 11. Knife of Dreams.epub<br>│   │   ├── 12. The Gathering Storm.epub<br>│   │   ├── 13. Towers of Midnight.epub<br>│   │   ├── 14. A Memory of Light.epub<br>│   │   └── The Wheel of Time Companion _ The People, Places and History of the Bestselling Series.epub<br>│   └── pdf<br>│       ├── A Crown Of Swords.pdf<br>│       ├── A Memory of Light.pdf<br>│       ├── Crossroads of Twilight.pdf<br>│       ├── Knife of Dreams.pdf<br>│       ├── The Dragon Reborn.pdf<br>│       ├── The Eye Of The World.pdf<br>│       ├── The Fires Of Heaven.pdf<br>│       ├── The Gathering Storm.pdf<br>│       ├── The Great Hunt.pdf<br>│       ├── The Lord Of Chaos.pdf<br>│       ├── The Path Of Daggers.pdf<br>│       ├── The Shadow Rising.pdf<br>│       ├── Towers of Midnight.pdf<br>│       └── Winter_s Heart.pdf<br>├── kube<br>│   ├── istio<br>│   │   ├── README.md<br>│   │   ├── install_istio.sh<br>│   │   └── uninstall_istio.sh<br>│   └── milvus<br>│       ├── README.md<br>│       ├── milvus-dashboard.json<br>│       └── values-standalone.yaml<br>└── notebook<br>    ├── poetry.lock<br>    └── pyproject.toml<br><br>8 directories, 38 files<br>tharindu@192 wheel-of-time-rag %<br><br>###########################################<br># Create a .env file with necessary environment variables<br># Create project files<br>touch notebook/.env<br><br># add this content to your .env file<br>OPENAI_API_KEY=your_openai_api_key<br>MILVUS_URI=http://localhost:19530<br>MILVUS_USER=username<br>MILVUS_PASSWORD=password<br><br># Install Python 3.11.10<br>pyenv install 3.11.10<br><br># Set local Python version for our project<br>cd notebook<br>pyenv local 3.11.10<br><br># Verify Python version<br>python --version  # Should show Python 3.11.10<br><br># Then install dependencies<br>poetry install<br># Activate the virtual environment<br>poetry shell</pre><h3>Wheel of Time Text Processing Pipeline</h3><p>The data processing pipeline is responsible for <strong>ingesting, structuring, and embedding</strong> the <em>Wheel of Time</em> book series for retrieval.</p><ul><li><strong>EPUB Files</strong>: The source data consists of EPUB versions of <em>The Wheel of Time</em> books.</li><li><strong>Text Processor</strong>: The system extracts raw text from EPUB files and processes it.</li><li><strong>RecursiveCharacterTextSplitter</strong>: This module <strong>splits the extracted text</strong> into smaller <strong>semantic chunks</strong> to improve retrieval accuracy.</li><li><strong>Extract Metadata</strong>: Each chunk is <strong>enriched with metadata</strong> (book name, chapter, section) to provide structured retrieval.</li><li><strong>OpenAI text-embedding-3-large</strong>: Each text chunk is converted into <strong>vector embeddings</strong>, allowing <strong>semantic similarity-based retrieval</strong> instead of keyword-based matching.</li><li>All <strong>vector embeddings</strong> are then stored in the <strong>Milvus Vector Database</strong> for efficient search.</li></ul><pre>&quot;&quot;&quot;<br>Wheel of Time Text Processing Pipeline<br><br>This module provides a comprehensive system for processing the Wheel of Time book series<br>into a format suitable for semantic search and retrieval. The system handles EPUB files,<br>extracts meaningful chunks of text, and enriches them with metadata about their location<br>and context within the books.<br><br>The processing pipeline follows these steps:<br>1. Load EPUB files using UnstructuredEPubLoader<br>2. Split text into semantic chunks using custom separators<br>3. Extract and track chapter information<br>4. Enrich chunks with metadata (book name, chapter details)<br><br>The chunking strategy is specially designed for the Wheel of Time series, taking into<br>account its specific formatting patterns and narrative structure. The system recognizes<br>various chapter heading formats, scene breaks, and natural text boundaries to create<br>meaningful chunks that preserve context.<br><br>Author: Tharindu Sankalpa<br>Date: 2nd February 2025<br>&quot;&quot;&quot;<br><br>import os<br>import re<br>from typing import List, Dict, Any, Optional, Tuple<br>from dataclasses import dataclass<br>from langchain_community.document_loaders import UnstructuredEPubLoader<br>from langchain.text_splitter import RecursiveCharacterTextSplitter<br>from langchain.docstore.document import Document<br><br><br>@dataclass<br>class ChapterInfo:<br>    &quot;&quot;&quot;<br>    Holds information about a chapter in the Wheel of Time series.<br>    <br>    Attributes:<br>        number: The chapter number, which can be a digit or special marker like &quot;PROLOGUE&quot;<br>        title: The chapter&#39;s title<br>    &quot;&quot;&quot;<br>    number: str<br>    title: str<br><br><br>class WheelOfTimeProcessor:<br>    &quot;&quot;&quot;<br>    A comprehensive processor for the Wheel of Time book series that converts EPUB files<br>    into searchable text chunks with metadata.<br>    <br>    This class handles the entire pipeline of processing Wheel of Time books:<br>    - Loading EPUB files<br>    - Splitting text into meaningful chunks<br>    - Tracking chapter information<br>    - Enriching chunks with metadata<br>    <br>    The processor is specifically tuned for the Wheel of Time series&#39; structure and<br>    formatting patterns, making it more accurate than general-purpose text processors.<br>    &quot;&quot;&quot;<br><br>    def __init__(self, chunk_size: int = 1000, chunk_overlap: int = 200):<br>        &quot;&quot;&quot;<br>        Initialize the Wheel of Time processor with customizable chunking parameters.<br><br>        The chunk size and overlap are crucial for creating meaningful text segments:<br>        - Larger chunks provide more context but may be too broad for precise retrieval<br>        - Smaller chunks are more precise but might miss broader context<br>        - Overlap helps maintain continuity between chunks<br><br>        Args:<br>            chunk_size: Target size of text chunks in characters. Default is 1000,<br>                       which typically captures 1-2 paragraphs of text.<br>            chunk_overlap: Number of characters to overlap between chunks. Default is 200,<br>                         which helps maintain context across chunk boundaries.<br>        &quot;&quot;&quot;<br>        self.chunk_size = chunk_size<br>        self.chunk_overlap = chunk_overlap<br><br>    def _create_text_splitter(self) -&gt; RecursiveCharacterTextSplitter:<br>        &quot;&quot;&quot;<br>        Create a specialized text splitter optimized for Wheel of Time books.<br><br>        The splitter uses a hierarchical set of separators that match the books&#39;<br>        structure, from major divisions (chapters, prologues) down to sentence-level<br>        breaks. This ensures that chunks align with natural narrative boundaries.<br><br>        The separator patterns handle variations in formatting:<br>        1. Chapter markers (CHAPTER, Chapter, etc.)<br>        2. Scene breaks (typically marked with asterisks)<br>        3. Natural language breaks (sentence endings, dialogue)<br>        4. Fallback separators for consistent chunking<br><br>        Returns:<br>            A configured RecursiveCharacterTextSplitter ready to process WoT text<br>        &quot;&quot;&quot;<br>        # Define section markers with case-insensitive matching<br>        section_patterns = [<br>            r&#39;\nEPILOGUE\s*\n&#39;,<br>            r&#39;\nepilogue\s*\n&#39;,<br>            r&#39;\nEpilogue\s*\n&#39;,<br>            r&#39;\nPROLOGUE\s*\n&#39;,<br>            r&#39;\nprologue\s*\n&#39;,<br>            r&#39;\nPrologue\s*\n&#39;,<br>            r&#39;\nCHAPTER\s*\d*\s*\n&#39;,<br>            r&#39;\nchapter\s*\d*\s*\n&#39;,<br>            r&#39;\nChapter\s*\d*\s*\n&#39;,<br>        ]<br>        <br>        # Combine with general text separators<br>        wot_separators = (<br>            section_patterns +<br>            [<br>                &quot;\n*   *   *\n&quot;,  # Scene breaks (asterisk pattern)<br>                &quot;\n\n&quot;,           # Paragraph breaks<br>                &quot;. &quot;,             # Sentence endings<br>                &quot;! &quot;,            # Exclamation endings<br>                &quot;? &quot;,            # Question endings<br>                &#39;&quot;\n&#39;,           # Dialogue line breaks<br>                &#39;&quot; &#39;,            # Dialogue<br>                &quot;\n&quot;,            # Basic line breaks<br>                &quot; &quot;,             # Word breaks<br>                &quot;&quot;               # Character-level fallback<br>            ]<br>        )<br>        <br>        return RecursiveCharacterTextSplitter(<br>            separators=wot_separators,<br>            chunk_size=self.chunk_size,<br>            chunk_overlap=self.chunk_overlap<br>        )<br><br>    def _extract_chapter_info(self, text: str) -&gt; ChapterInfo:<br>        &quot;&quot;&quot;<br>        Extract chapter number and title from text using sophisticated pattern matching.<br><br>        This method handles the various ways chapters are formatted across the series,<br>        including:<br>        - Standard numbered chapters: &quot;CHAPTER 8\nWhirlpools of Color&quot;<br>        - Multi-line formats: &quot;CHAPTER\n39\nThose Who Fight&quot;<br>        - Special sections: &quot;PROLOGUE\nDragonmount&quot;<br>        - Variant capitalizations and spacing<br><br>        The method uses a series of regular expressions in decreasing order of<br>        specificity, trying to extract as much information as possible from the<br>        text.<br><br>        Args:<br>            text: The text to analyze for chapter information<br><br>        Returns:<br>            ChapterInfo object containing the chapter number and title<br>        &quot;&quot;&quot;<br>        chapter_patterns = [<br>            # Standard format: &quot;CHAPTER 8\nWhirlpools of Color&quot;<br>            r&#39;(?i)(?:CHAPTER|Chapter)\s*(\d+)\s*\n+([^\n]+)&#39;,<br>            <br>            # Split format: &quot;CHAPTER\n39\nThose Who Fight&quot;<br>            r&#39;(?i)(?:CHAPTER|Chapter)\s*\n+(\d+)\s*\n+([^\n]+)&#39;,<br>            <br>            # Multi-newline format: &quot;CHAPTER 8\n\nWhirlpools of Color&quot;<br>            r&#39;(?i)(?:CHAPTER|Chapter)\s*(\d+)\s*\n+\s*\n+([^\n]+)&#39;,<br>            <br>            # Written numbers: &quot;CHAPTER EIGHT: The Title&quot;<br>            r&#39;(?i)(?:CHAPTER|Chapter)\s*([A-Za-z0-9]+):\s*([^\n]+)&#39;,<br>            <br>            # Simple title: &quot;CHAPTER: The Title&quot;<br>            r&#39;(?i)(?:CHAPTER|Chapter):\s*([^\n]+)&#39;,<br>            <br>            # Special sections: &quot;PROLOGUE\nThe Title&quot;<br>            r&#39;(?i)(?:PROLOGUE|Prologue|EPILOGUE|Epilogue)\s*\n+([^\n]+)&#39;,<br>            <br>            # Bare chapters: &quot;CHAPTER 8&quot; or &quot;PROLOGUE&quot;<br>            r&#39;(?i)(?:CHAPTER|Chapter)\s*(\d+)\s*$&#39;,<br>            r&#39;(?i)(?:PROLOGUE|Prologue|EPILOGUE|Epilogue)\s*$&#39;<br>        ]<br>        <br>        for pattern in chapter_patterns:<br>            match = re.search(pattern, text)<br>            if match:<br>                groups = match.groups()<br>                if len(groups) == 2:<br>                    return ChapterInfo(<br>                        number=groups[0],<br>                        title=groups[1].strip()<br>                    )<br>                elif len(groups) == 1:<br>                    return ChapterInfo(<br>                        number=&quot;N/A&quot;,<br>                        title=groups[0].strip()<br>                    )<br>        <br>        return ChapterInfo(number=&quot;N/A&quot;, title=&quot;N/A&quot;)<br><br>    def process_epub(self, epub_path: str) -&gt; List[Document]:<br>        &quot;&quot;&quot;<br>        Process a single Wheel of Time EPUB file into enriched text chunks.<br><br>        This method performs the complete processing pipeline for a single book:<br>        1. Loads the EPUB file<br>        2. Extracts the book name<br>        3. Splits the text into chunks<br>        4. Tracks chapter information across chunks<br>        5. Enriches chunks with metadata<br><br>        The method maintains chapter context across chunk boundaries, ensuring that<br>        each chunk knows which chapter it belongs to, even if it doesn&#39;t contain<br>        a chapter heading itself.<br><br>        Args:<br>            epub_path: Path to the EPUB file to process<br><br>        Returns:<br>            List of Document objects, each containing a text chunk and its metadata<br><br>        Example:<br>            processor = WheelOfTimeProcessor()<br>            chunks = processor.process_epub(&quot;eye_of_the_world.epub&quot;)<br>        &quot;&quot;&quot;<br>        # Load the EPUB<br>        loader = UnstructuredEPubLoader(epub_path)<br>        data = loader.load()<br>        <br>        # Extract book name from path<br>        book_name = os.path.basename(epub_path).replace(&#39;.epub&#39;, &#39;&#39;)<br>        <br>        # Create text splitter and split document<br>        text_splitter = self._create_text_splitter()<br>        chunks = text_splitter.split_documents(data)<br>        <br>        # Initialize chapter tracking<br>        current_chapter = ChapterInfo(number=&quot;N/A&quot;, title=&quot;N/A&quot;)<br>        <br>        # Process each chunk<br>        for chunk in chunks:<br>            # Check for new chapter information<br>            chapter_info = self._extract_chapter_info(chunk.page_content)<br>            if chapter_info.number != &quot;N/A&quot; or chapter_info.title != &quot;N/A&quot;:<br>                current_chapter = chapter_info<br>            <br>            # Enrich chunk with metadata<br>            chunk.metadata.update({<br>                &quot;book_name&quot;: book_name,<br>                &quot;chapter_number&quot;: current_chapter.number,<br>                &quot;chapter_title&quot;: current_chapter.title<br>            })<br>        <br>        return chunks<br><br>    def process_series(self, epub_dir: str) -&gt; List[Document]:<br>        &quot;&quot;&quot;<br>        Process the entire Wheel of Time series from a directory of EPUB files.<br><br>        This method processes all EPUB files in the specified directory, combining<br>        them into a single collection of chunks while maintaining proper book and<br>        chapter context for each chunk.<br><br>        Args:<br>            epub_dir: Directory containing Wheel of Time EPUB files<br><br>        Returns:<br>            List of all processed chunks across the entire series<br><br>        Example:<br>            processor = WheelOfTimeProcessor()<br>            chunks = processor.process_series(&quot;wheel_of_time_epubs/&quot;)<br>            print(f&quot;Processed {len(chunks)} chunks across the series&quot;)<br>        &quot;&quot;&quot;<br>        all_chunks = []<br>        <br>        for filename in sorted(os.listdir(epub_dir)):<br>            if filename.endswith(&#39;.epub&#39;):<br>                epub_path = os.path.join(epub_dir, filename)<br>                try:<br>                    chunks = self.process_epub(epub_path)<br>                    all_chunks.extend(chunks)<br>                    print(f&quot;Processed {filename}: {len(chunks)} chunks created&quot;)<br>                except Exception as e:<br>                    print(f&quot;Error processing {filename}: {str(e)}&quot;)<br>        <br>        return all_chunks<br><br><br># Example usage:<br>if __name__ == &quot;__main__&quot;:<br>    # Initialize processor with custom chunk parameters<br>    processor = WheelOfTimeProcessor(chunk_size=1000, chunk_overlap=200)<br>    <br>    # Process the entire series (Set your path)<br>    epub_dir = &quot;/Users/tharindu/git/wheel-of-time-rag/data/epub&quot;<br>    chunks = processor.process_series(epub_dir)<br>    <br>    print(f&quot;Total chunks processed: {len(chunks)}&quot;)</pre><pre>Processed 00. New Spring.epub: 953 chunks created<br>Processed 01. The Eye of the World.epub: 2268 chunks created<br>Processed 02. The Great Hunt.epub: 1948 chunks created<br>Processed 03. The Dragon Reborn.epub: 1839 chunks created<br>Processed 04. The Shadow Rising.epub: 2931 chunks created<br>Processed 05. The Fires of Heaven.epub: 2672 chunks created<br>Processed 06. Lord of Chaos.epub: 3091 chunks created<br>Processed 07. A Crown of Swords.epub: 2297 chunks created<br>Processed 08. The Path of Daggers.epub: 1799 chunks created<br>Processed 09. Winter_s Heart.epub: 1900 chunks created<br>Processed 10. Crossroads of Twilight.epub: 2164 chunks created<br>Processed 11. Knife of Dreams.epub: 2524 chunks created<br>Processed 12. The Gathering Storm.epub: 2207 chunks created<br>Processed 13. Towers of Midnight.epub: 2381 chunks created<br>Processed 14. A Memory of Light.epub: 2514 chunks created<br>Processed The Wheel of Time Companion _ The People, Places and History of the Bestselling Series.epub: 3134 chunks created<br>Total chunks processed: 36622</pre><h3>Vector Store (Milvus Infrastructure)</h3><p>Milvus stands out as an excellent choice for vector database needs, offering several deployment options to match different use cases. Let’s explore why Milvus was the perfect fit for this project.</p><p><strong>Understanding Milvus Deployment Options</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jqQPxvPy2cuhQdsvsV4OMA.png" /></figure><p>Milvus offers three deployment modes, each suited for different scales and needs:</p><ol><li><strong>Milvus Lite</strong>: A lightweight version installable via pip, perfect for notebooks and prototyping. It’s essentially a vector database as a library, making it ideal for learning and initial development.</li><li><strong>Milvus Standalone</strong>: A single-node deployment that provides a complete vector database suitable for production or testing environments. It efficiently handles up to millions of vectors, making it perfect for small to medium-scale applications.</li><li><strong>Milvus Distributed</strong>: Designed for large-scale deployments handling over 100 million vectors. It requires multiple nodes and replicas for high availability and scalability.</li></ol><p><strong>Implementing Milvus Standalone</strong></p><p>Setting up Milvus in a Kubernetes environment requires careful preparation and understanding of several components. Let’s walk through the entire process, starting with the infrastructure requirements and moving to the final deployment steps.</p><h4>Prerequisites</h4><h4><strong>1. Kubernetes Cluster Setup</strong></h4><p>You’ll need a running Kubernetes cluster. You have several options:</p><p><strong>Minikube (for local development)</strong>:</p><pre>####################################<br># Download and install Minikube<br>curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64<br>sudo install minikube-linux-amd64 /usr/local/bin/minikube<br><br># Start Minikube with Docker driver<br>minikube start --driver=docker \<br>    --cpus=4 \<br>    --memory=8g \<br>    --disk-size=20g<br><br># Enable useful addons<br>minikube addons enable metrics-server<br>minikube addons enable dashboard<br><br># Check Minikube and kubectl<br>minikube status<br>kubectl get nodes</pre><p><strong>Cloud Provider Options</strong>:</p><ul><li>Google Kubernetes Engine (GKE):</li></ul><pre># Check GCP account list<br>gcloud auth list<br><br># Select the account<br>gcloud config set account `ACCOUNT`<br><br># Create a GKE cluster<br>gcloud container clusters create milvus-cluster \<br>  --num-nodes=1 \<br>  --machine-type=e2-standard-4 \<br>  --disk-type=pd-ssd \<br>  --disk-size=50GB \<br>  --zone=asia-south1-a \<br>  --cluster-version=latest</pre><ul><li>Azure Kubernetes Service (AKS):</li></ul><pre># Login to Azure<br>az login<br><br># Optional: List available subscriptions<br>az account list --output table<br><br># Select specific subscription if needed<br>az account set --subscription &lt;subscription-id&gt;<br><br># Create resource group in East US region<br>az group create \<br>    --name my-mlivus-rg \<br>    --location centerlindia<br><br># Create AKS cluster with autoscaling enabled<br>az aks create \<br>    --resource-group my-mlivus-rg \<br>    --name my-mlivus-aks \<br>    --location centerlindia \<br>    --node-count 1 \<br>    --node-vm-size Standard_D4ads_v5 \<br>    --enable-cluster-autoscaler \<br>    --min-count 1 \<br>    --max-count 3\<br>    --generate-ssh-keys</pre><ul><li>Amazon Elastic Kubernetes Service (EKS):</li></ul><pre># Create an EKS cluster using eksctl<br>eksctl create cluster \<br>  --name milvus-cluster \<br>  --region us-west-2 \<br>  --node-type t3.xlarge \<br>  --nodes 1</pre><h4>2. kubectl Configuration</h4><p>Ensure kubectl is properly configured to communicate with your cluster:</p><pre># For Minikube<br>minikube kubectl -- get nodes<br><br># For GKE<br>gcloud container clusters get-credentials milvus-cluster --region asia-south1-a<br><br># For AKS<br>az aks get-credentials \<br>    --resource-group my-mlivus-rg \<br>    --name my-mlivus-aks<br><br># For EKS<br>aws eks update-kubeconfig --name milvus-cluster --region us-west-2<br><br># Verify connection<br>kubectl cluster-info</pre><h4>3. Helm Installation</h4><p>Install Helm, the Kubernetes package manager:</p><pre># Download and install Helm<br>curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash<br><br># Verify installation<br>helm version</pre><h4>Milvus Configuration</h4><h4>1. Create the values-standalone.yaml File</h4><p>Create a new file named values-standalone.yaml in your working directory:</p><pre># values-standalone.yaml<br><br># First, we disable cluster mode and enable standalone<br>cluster:<br>  enabled: false<br><br># Configure the standalone deployment<br>standalone:<br>  enabled: true<br>  replicas: 1  # Only one replica needed for standalone<br>  resources:<br>    requests:<br>      cpu: &quot;1&quot;<br>      memory: &quot;4Gi&quot;  # Sufficient for 36K vectors<br>    limits:<br>      cpu: &quot;2&quot;<br>      memory: &quot;8Gi&quot;<br>  messageQueue: rocksmq  # Use RocksDB for message queue instead of Pulsar<br>  persistence:<br>    enabled: true<br>    size: &quot;20Gi&quot;  # Sufficient for your dataset<br>    <br># Configure etcd - needed even in standalone mode<br>etcd:<br>  enabled: true<br>  replicaCount: 1  # Only one replica needed for standalone<br>  resources:<br>    requests:<br>      memory: &quot;512Mi&quot;<br>      cpu: &quot;200m&quot;<br>    limits:<br>      memory: &quot;1Gi&quot;<br>      cpu: &quot;500m&quot;<br>  persistence:<br>    enabled: true<br>    size: &quot;2Gi&quot;  # Reduced size since it&#39;s just metadata<br><br># Configure MinIO for storage<br>minio:<br>  enabled: true<br>  mode: standalone  # Switch to standalone mode<br>  replicas: 1      # Only one replica needed<br>  resources:<br>    requests:<br>      memory: &quot;1Gi&quot;<br>      cpu: &quot;200m&quot;<br>    limits:<br>      memory: &quot;2Gi&quot;<br>      cpu: &quot;500m&quot;<br>  persistence:<br>    enabled: true<br>    size: &quot;20Gi&quot;  # Sufficient for your dataset<br><br># Disable Pulsar since we&#39;re using RocksDB<br>pulsar:<br>  enabled: false<br>pulsarv3:<br>  enabled: false<br><br># Disable all distributed components<br>mixCoordinator:<br>  enabled: false<br>rootCoordinator:<br>  enabled: false<br>queryCoordinator:<br>  enabled: false<br>indexCoordinator:<br>  enabled: false<br>dataCoordinator:<br>  enabled: false<br>queryNode:<br>  enabled: false<br>indexNode:<br>  enabled: false<br>dataNode:<br>  enabled: false<br>proxy:<br>  enabled: false<br><br># Configure the main service<br>service:<br>  type: ClusterIP<br>  port: 19530<br><br># Configure metrics for monitoring (optional but recommended)<br>metrics:<br>  enabled: true<br>  serviceMonitor:<br>    enabled: false  # Set to true if you have Prometheus operator<br><br># Basic probes configuration<br>livenessProbe:<br>  enabled: true<br>  initialDelaySeconds: 30<br>  periodSeconds: 15<br>  timeoutSeconds: 10<br>  failureThreshold: 3<br><br>readinessProbe:<br>  enabled: true<br>  initialDelaySeconds: 30<br>  periodSeconds: 15<br>  timeoutSeconds: 10<br>  failureThreshold: 3<br><br># Enble RBAC<br>extraConfigFiles:<br>  user.yaml: |+<br>    common:<br>      security:<br>        authorizationEnabled: true</pre><p>The configuration above is optimized for a standalone deployment with:</p><ul><li>Sufficient resources for development and testing</li><li>Persistent storage enabled for data durability</li><li>RocksDB-based message queue for simplified deployment</li><li>Minimal resource requirements suitable for most development environments</li></ul><h4>Deployment Steps</h4><pre># Add the Milvus Helm Repository<br>helm repo add milvus https://zilliztech.github.io/milvus-helm<br>helm repo update<br><br># Create a Namespace (Optional but Recommended)<br>kubectl create namespace milvus-system<br><br># Deploy Milvus<br>helm install my-release milvus/milvus \<br>  -f values-standalone.yaml \<br>  --namespace milvus-system \<br>  --wait<br><br># Set Up Port Forwarding<br># Forward the Milvus service ports<br>kubectl port-forward service/my-release-milvus 9091:9091 -n milvus-system &amp;<br>kubectl port-forward service/my-release-milvus 19530:19530 -n milvus-system &amp;</pre><h4>Verification Steps</h4><h4>1. Check Pod Status</h4><pre>tharindu@192 milvus % kubectl get all -n milvus-system<br>NAME                                                READY   STATUS    RESTARTS   AGE<br>pod/my-release-etcd-0                               1/1     Running   0          2m16s<br>pod/my-release-milvus-standalone-77b888cd8f-f9fn4   1/1     Running   0          2m16s<br>pod/my-release-minio-8d446bf78-v6dcp                1/1     Running   0          2m16s<br><br>NAME                               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)              AGE<br>service/my-release-etcd            ClusterIP   34.118.229.67    &lt;none&gt;        2379/TCP,2380/TCP    2m16s<br>service/my-release-etcd-headless   ClusterIP   None             &lt;none&gt;        2379/TCP,2380/TCP    2m16s<br>service/my-release-milvus          ClusterIP   34.118.238.226   &lt;none&gt;        19530/TCP,9091/TCP   2m16s<br>service/my-release-minio           ClusterIP   34.118.233.162   &lt;none&gt;        9000/TCP             2m16s<br><br>NAME                                           READY   UP-TO-DATE   AVAILABLE   AGE<br>deployment.apps/my-release-milvus-standalone   1/1     1            1           2m16s<br>deployment.apps/my-release-minio               1/1     1            1           2m16s<br><br>NAME                                                      DESIRED   CURRENT   READY   AGE<br>replicaset.apps/my-release-milvus-standalone-77b888cd8f   1         1         1       2m16s<br>replicaset.apps/my-release-minio-8d446bf78                1         1         1       2m16s<br><br>NAME                               READY   AGE<br>statefulset.apps/my-release-etcd   1/1     2m17s<br>tharindu@192 milvus %</pre><p>All pods should show Running status and be ready (1/1).</p><h4>2. Verify Web UI Access</h4><p>Open <a href="http://localhost:9091/webui">http://localhost:9091/webui</a> in your browser. You should see the Milvus Web Interface.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d8W1OMvTbglsBAzbVxpMgQ.png" /><figcaption>Milvus Web UI</figcaption></figure><h4>3. Check Service Health</h4><pre># Check all services<br>kubectl get svc -n milvus-system<br><br># Check logs for any errors<br>kubectl logs -l app.kubernetes.io/instance=my-release -n milvus-system</pre><h4>Troubleshooting Common Issues</h4><pre># Insufficient Resources: Look for events indicating resource constraints.<br>kubectl describe pods -n milvus-system<br><br># Persistent Volume Issues:Ensure PVCs are bound to PVs.<br>kubectl get pv,pvc -n milvus-system<br><br># Pod Startup Issues: Check events and container statuses.<br>kubectl describe pod &lt;pod-name&gt; -n milvus-system</pre><h4>Next Steps</h4><p>After successful deployment, you can:</p><ol><li>Configure your application to connect to Milvus using port 19530</li><li>Use the Web UI to manage collections and indexes</li><li>Monitor system performance through Kubernetes dashboard or monitoring tools</li></ol><h4>Clean Up</h4><p>If you need to remove the deployment:</p><pre>helm uninstall my-release -n milvus-system<br>kubectl delete namespace milvus-system</pre><p>This setup provides a production-ready Milvus instance suitable for development and testing. For production environments, you might want to adjust resource limits and enable additional security features.</p><h3>Milvus Database Operations Utility</h3><p>To simplify these essential tasks, I have developed a Python utility script that encapsulates key Milvus functionalities. This script provides a user-friendly interface to manage databases, handle collections, and execute vector queries efficiently.</p><p>This utility streamlines Milvus database interactions, allowing seamless <strong>connection, management, and query execution</strong>. Below is the implementation, which ensures efficient handling of vector-based data storage and retrieval.</p><pre>&quot;&quot;&quot;<br>Milvus Database Operations Utility<br><br>This script provides a simple interface for common Milvus vector database operations.<br>It includes functions for:<br>- Database connection and management<br>- User management<br>- Collection operations<br>- Data operations (querying, deletion)<br><br>Usage:<br>    from milvus_ops import MilvusOps<br>    <br>    # Initialize with connection details<br>    milvus = MilvusOps(<br>        uri=&quot;http://localhost:19530&quot;,<br>        user=&quot;root&quot;,<br>        password=&quot;Milvus&quot;<br>    )<br>    <br>    # Perform operations<br>    milvus.create_database(&quot;my_database&quot;)<br>    milvus.list_collections()<br><br>The script uses environment variables for sensitive information:<br>- MILVUS_URI: Database connection URI<br>- MILVUS_USER: Username for authentication<br>- MILVUS_PASSWORD: Password for authentication<br><br>Author: Tharindu Sankalpa<br>Date: 2nd February 2025<br>&quot;&quot;&quot;<br><br>import os<br>from typing import List, Dict, Optional, Union<br>from pymilvus import connections, MilvusClient, utility<br><br><br>class MilvusOps:<br>    def __init__(<br>        self,<br>        uri: str = &quot;http://localhost:19530&quot;,<br>        user: str = &quot;root&quot;,<br>        password: str = &quot;Milvus&quot;,<br>        db_name: str = &quot;default&quot;<br>    ):<br>        &quot;&quot;&quot;<br>        Initialize Milvus operations with connection details.<br><br>        Args:<br>            uri: Milvus server URI<br>            user: Username for authentication<br>            password: Password for authentication<br>            db_name: Target database name<br>        &quot;&quot;&quot;<br>        self.uri = uri<br>        self.user = user<br>        self.password = password<br>        self.db_name = db_name<br>        self.client = None<br>        self.connect()<br><br>    def connect(self) -&gt; None:<br>        &quot;&quot;&quot;<br>        Establish connection to Milvus server.<br>        Creates a new MilvusClient instance for database operations.<br>        &quot;&quot;&quot;<br>        try:<br>            self.client = MilvusClient(<br>                uri=self.uri,<br>                token=f&quot;{self.user}:{self.password}&quot;,<br>                db_name=self.db_name<br>            )<br>            print(f&quot;Connected to Milvus database: {self.db_name}&quot;)<br>        except Exception as e:<br>            print(f&quot;Connection failed: {str(e)}&quot;)<br>            raise<br><br>    def disconnect(self) -&gt; None:<br>        &quot;&quot;&quot;Close the Milvus connection.&quot;&quot;&quot;<br>        if self.client:<br>            connections.disconnect(self.db_name)<br>            self.client = None<br>            print(&quot;Disconnected from Milvus&quot;)<br><br>    def create_database(self, db_name: str) -&gt; bool:<br>        &quot;&quot;&quot;<br>        Create a new database in Milvus.<br><br>        Args:<br>            db_name: Name of the database to create<br><br>        Returns:<br>            bool: True if successful, False otherwise<br>        &quot;&quot;&quot;<br>        try:<br>            connections.connect(<br>                uri=self.uri,<br>                token=f&quot;{self.user}:{self.password}&quot;,<br>                db_name=&quot;default&quot;<br>            )<br>            from pymilvus import db<br>            db.create_database(db_name)<br>            print(f&quot;Created database: {db_name}&quot;)<br>            return True<br>        except Exception as e:<br>            print(f&quot;Failed to create database: {str(e)}&quot;)<br>            return False<br>        finally:<br>            connections.disconnect(&quot;default&quot;)<br><br>    def list_collections(self) -&gt; List[str]:<br>        &quot;&quot;&quot;<br>        Get list of all collections in the current database.<br><br>        Returns:<br>            List[str]: Collection names<br>        &quot;&quot;&quot;<br>        try:<br>            collections = self.client.list_collections()<br>            return collections<br>        except Exception as e:<br>            print(f&quot;Failed to list collections: {str(e)}&quot;)<br>            return []<br><br>    def delete_collection(self, collection_name: str) -&gt; bool:<br>        &quot;&quot;&quot;<br>        Delete a collection from the database.<br><br>        Args:<br>            collection_name: Name of collection to delete<br><br>        Returns:<br>            bool: True if successful, False otherwise<br>        &quot;&quot;&quot;<br>        try:<br>            self.client.drop_collection(collection_name)<br>            print(f&quot;Deleted collection: {collection_name}&quot;)<br>            return True<br>        except Exception as e:<br>            print(f&quot;Failed to delete collection: {str(e)}&quot;)<br>            return False<br><br>    def delete_vectors(<br>        self,<br>        collection_name: str,<br>        filter_condition: str<br>    ) -&gt; Dict[str, int]:<br>        &quot;&quot;&quot;<br>        Delete vectors from a collection based on a filter condition.<br><br>        Args:<br>            collection_name: Target collection name<br>            filter_condition: Filter expression (e.g., &quot;page &gt;= 200 and page &lt;= 220&quot;)<br><br>        Returns:<br>            Dict[str, int]: Deletion results with delete count<br>        &quot;&quot;&quot;<br>        try:<br>            result = self.client.delete(<br>                collection_name=collection_name,<br>                filter=filter_condition<br>            )<br>            # Ensure changes are persisted<br>            self.client.flush(collection_name)<br>            return result<br>        except Exception as e:<br>            print(f&quot;Failed to delete vectors: {str(e)}&quot;)<br>            return {&quot;delete_count&quot;: 0}<br><br>    def query_vectors(<br>        self,<br>        collection_name: str,<br>        filter_condition: str,<br>        limit: int = 10<br>    ) -&gt; List[Dict]:<br>        &quot;&quot;&quot;<br>        Query vectors from a collection using a filter condition.<br><br>        Args:<br>            collection_name: Target collection name<br>            filter_condition: Filter expression<br>            limit: Maximum number of results to return<br><br>        Returns:<br>            List[Dict]: Query results<br>        &quot;&quot;&quot;<br>        try:<br>            results = self.client.query(<br>                collection_name=collection_name,<br>                filter=filter_condition,<br>                limit=limit<br>            )<br>            return results<br>        except Exception as e:<br>            print(f&quot;Failed to query vectors: {str(e)}&quot;)<br>            return []<br><br>    def __enter__(self):<br>        &quot;&quot;&quot;Context manager entry.&quot;&quot;&quot;<br>        return self<br><br>    def __exit__(self, exc_type, exc_val, exc_tb):<br>        &quot;&quot;&quot;Context manager exit with automatic disconnection.&quot;&quot;&quot;<br>        self.disconnect()<br><br><br>if __name__ == &quot;__main__&quot;:<br>    # Example usage<br>    milvus = MilvusOps(<br>        uri=&quot;http://localhost:19530&quot;,<br>        user=&quot;root&quot;,<br>        password=&quot;Milvus&quot;,<br>        db_name=&quot;default&quot;<br>    )<br><br>    # List collections<br>    collections = milvus.list_collections()<br>    print(f&quot;Available collections: {collections}&quot;)<br><br>    # Create a new database<br>    milvus.create_database(&quot;wheel_of_time_db&quot;)<br><br>    # Clean up<br>    milvus.disconnect()</pre><h3>Deploy Istio Service Mesh and Observability Stack</h3><p>Now, let’s set up the Istio service mesh with its native observability stack, which includes Prometheus, Grafana, Kiali, and Jaeger. In the <strong>kube/istio</strong> directory, I have created a bash script — be sure to provide it with execution permission before running it. For this script to work, you must have your <strong>kubectl</strong> context correctly configured for your Kubernetes cluster. The script will install all the Istio components along with <strong>Grafana, Prometheus, Kiali, and Jaeger</strong>, and it will verify the installation upon completion.</p><p>The primary goal of integrating this observability stack into the Wheel of Time RAG system is to collect comprehensive observability details from the <strong>Milvus</strong> database using <strong>Prometheus</strong>. Prometheus will scrape all the default metrics exposed by Milvus, enabling us to create a customized <strong>Grafana</strong> dashboard that monitors key performance indicators and other vital observability information related to our Milvus instance.</p><pre>cd kube/istio<br>chmod +x install_istio.sh<br>./install_istio.sh</pre><pre>rag-pdf-qa-backend-py3.11tharindu@192 istio % chmod +x install_istio.sh <br>rag-pdf-qa-backend-py3.11tharindu@192 istio % ./install_istio.sh <br>[2025-02-03 09:23:46] INFO: Checking prerequisites...<br>[2025-02-03 09:23:47] INFO: Installing Istio CLI version 1.24.2<br>  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current<br>                                 Dload  Upload   Total   Spent    Left  Speed<br>100   101  100   101    0     0    360      0 --:--:-- --:--:-- --:--:--   360<br>100  5124  100  5124    0     0   6690      0 --:--:-- --:--:-- --:--:-- 16113<br><br>Downloading istio-1.24.2 from https://github.com/istio/istio/releases/download/1.24.2/istio-1.24.2-osx-arm64.tar.gz ...<br><br>Istio 1.24.2 download complete!<br><br>The Istio release archive has been downloaded to the istio-1.24.2 directory.<br><br>To configure the istioctl client tool for your workstation,<br>add the /Users/tharindu/git/rag-pdf-qa-backend/kube/istio/istio-1.24.2/bin directory to your environment path variable with:<br>         export PATH=&quot;$PATH:/Users/tharindu/git/rag-pdf-qa-backend/kube/istio/istio-1.24.2/bin&quot;<br><br>Begin the Istio pre-installation check by running:<br>         istioctl x precheck <br><br>Try Istio in ambient mode<br>        https://istio.io/latest/docs/ambient/getting-started/<br>Try Istio in sidecar mode<br>        https://istio.io/latest/docs/setup/getting-started/<br>Install guides for ambient mode<br>        https://istio.io/latest/docs/ambient/install/<br>Install guides for sidecar mode<br>        https://istio.io/latest/docs/setup/install/<br><br>Need more information? Visit https://istio.io/latest/docs/ <br>[2025-02-03 09:23:52] INFO: Creating Istio configuration file<br>[2025-02-03 09:23:52] INFO: Adding Istio CLI to PATH<br>[2025-02-03 09:23:52] INFO: Verifying Istio CLI installation<br>Istio is not present in the cluster: no running Istio pods in namespace &quot;istio-system&quot;<br>client version: 1.24.2<br>[2025-02-03 09:23:54] INFO: Creating Istio namespace<br>namespace/istio-system created<br>[2025-02-03 09:23:55] INFO: Installing Istio with configuration<br>        |\          <br>        | \         <br>        |  \        <br>        |   \       <br>      /||    \      <br>     / ||     \     <br>    /  ||      \    <br>   /   ||       \   <br>  /    ||        \  <br> /     ||         \ <br>/______||__________\<br>____________________<br>  \__       _____/  <br>     \_____/        <br><br>This will install the Istio 1.24.2 profile &quot;default&quot; into the cluster. Proceed? (y/N) y<br><br>- Processing resources for Istio core.<br>✔ Istio core installed ⛵️<br>- Processing resources for Istiod.<br>- Processing resources for Istiod. Waiting for Deployment/istio-system/istiod<br>✔ Istiod installed 🧠<br>- Processing resources for CNI, Egress gateways, Ingress gateways.<br>- Processing resources for CNI, Egress gateways, Ingress gateways. Waiting for DaemonSet/kube-system/istio-cni-node<br>- Processing resources for CNI, Egress gateways, Ingress gateways. Waiting for DaemonSet/kube-system/istio-cni-node, Deployment/istio-system/i...<br>✔ CNI installed 🪢<br>✔ Egress gateways installed 🛫<br>✔ Ingress gateways installed 🛬<br>- Pruning removed resources<br>✔ Installation complete[2025-02-03 09:24:38] INFO: Labeling default namespace with istio-injection=enabled<br>namespace/default labeled<br>[2025-02-03 09:24:38] INFO: Installing prometheus<br>serviceaccount/prometheus created<br>configmap/prometheus created<br>clusterrole.rbac.authorization.k8s.io/prometheus created<br>clusterrolebinding.rbac.authorization.k8s.io/prometheus created<br>service/prometheus created<br>deployment.apps/prometheus created<br>[2025-02-03 09:24:42] INFO: Installing grafana<br>serviceaccount/grafana created<br>configmap/grafana created<br>service/grafana created<br>deployment.apps/grafana created<br>configmap/istio-grafana-dashboards created<br>configmap/istio-services-grafana-dashboards created<br>[2025-02-03 09:24:46] INFO: Installing jaeger<br>deployment.apps/jaeger created<br>service/tracing created<br>service/zipkin created<br>service/jaeger-collector created<br>[2025-02-03 09:24:49] INFO: Installing kiali<br>serviceaccount/kiali created<br>configmap/kiali created<br>clusterrole.rbac.authorization.k8s.io/kiali created<br>clusterrolebinding.rbac.authorization.k8s.io/kiali created<br>service/kiali created<br>deployment.apps/kiali created<br>[2025-02-03 09:24:52] INFO: Waiting for all pods to be ready in istio-system namespace (timeout: 5m)<br>pod/grafana-6b45c49476-562fs condition met<br>pod/istio-egressgateway-fdd6c7bf-flvjh condition met<br>pod/istio-ingressgateway-79ddd4cf94-tnqqr condition met<br>pod/istiod-6fbf849d94-q92tv condition met<br>pod/jaeger-54c44d879f-7tk8f condition met<br>pod/kiali-79b6d98d5d-4jhxq condition met<br>pod/prometheus-6dd9fd5446-l5z4k condition met<br>[2025-02-03 09:25:23] INFO: Displaying pod status:<br>NAME                                    READY   STATUS    RESTARTS   AGE<br>grafana-6b45c49476-562fs                1/1     Running   0          39s<br>istio-egressgateway-fdd6c7bf-flvjh      1/1     Running   0          58s<br>istio-ingressgateway-79ddd4cf94-tnqqr   1/1     Running   0          58s<br>istiod-6fbf849d94-q92tv                 1/1     Running   0          71s<br>jaeger-54c44d879f-7tk8f                 1/1     Running   0          36s<br>kiali-79b6d98d5d-4jhxq                  1/1     Running   0          32s<br>prometheus-6dd9fd5446-l5z4k             2/2     Running   0          42s<br>[2025-02-03 09:25:24] INFO: Displaying service status:<br>NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                                          AGE<br>grafana                ClusterIP      34.118.233.241   &lt;none&gt;         3000/TCP                                         41s<br>istio-egressgateway    ClusterIP      34.118.238.153   &lt;none&gt;         80/TCP,443/TCP                                   60s<br>istio-ingressgateway   LoadBalancer   34.118.233.108   34.47.195.43   15021:30128/TCP,80:32625/TCP,443:31182/TCP       60s<br>istiod                 ClusterIP      34.118.225.9     &lt;none&gt;         15010/TCP,15012/TCP,443/TCP,15014/TCP            72s<br>jaeger-collector       ClusterIP      34.118.226.3     &lt;none&gt;         14268/TCP,14250/TCP,9411/TCP,4317/TCP,4318/TCP   36s<br>kiali                  ClusterIP      34.118.226.206   &lt;none&gt;         20001/TCP,9090/TCP                               33s<br>prometheus             ClusterIP      34.118.230.100   &lt;none&gt;         9090/TCP                                         43s<br>tracing                ClusterIP      34.118.239.98    &lt;none&gt;         80/TCP,16685/TCP                                 37s<br>zipkin                 ClusterIP      34.118.227.231   &lt;none&gt;         9411/TCP                                         36s<br>[2025-02-03 09:25:25] INFO: Istio installation completed successfully!<br>[2025-02-03 09:25:25] INFO: Script execution completed. Logs available in: /Users/tharindu/git/rag-pdf-qa-backend/kube/istio/istio_install_logs<br>rag-pdf-qa-backend-py3.11tharindu@192 istio % </pre><h3>Mivlus Vector Database Operations Monitoring</h3><p>Our current setup has a standard Prometheus instance (not the Operator) running in the <strong>istio-system</strong> namespace with Kubernetes service discovery. Since we are not using the Prometheus Operator, there are no Prometheus <strong>Custom Resource Definitions (CRDs)</strong> installed in the cluster.</p><p>To configure Prometheus to scrape <strong>Milvus built-in metrics</strong>, we need to add annotations to the <strong>Milvus service (my-release-milvus)</strong>. Milvus exposes metrics on <strong>port 9091/metrics</strong>.</p><pre>kubectl port-forward svc/my-release-milvus 9091:9091 -n milvus-system &amp;<br>curl localhost:9091/metrics</pre><pre>tharindu@192 ~ % kubectl port-forward svc/my-release-milvus 9091:9091 -n milvus-system &amp;<br>[1] 63656<br>tharindu@192 ~ % Forwarding from 127.0.0.1:9091 -&gt; 9091<br>Forwarding from [::1]:9091 -&gt; 9091<br><br>tharindu@192 ~ %<br>tharindu@192 ~ % curl localhost:9091/metrics<br>Handling connection for 9091<br># HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.<br># TYPE go_gc_duration_seconds summary<br>go_gc_duration_seconds{quantile=&quot;0&quot;} 9.1143e-05<br>go_gc_duration_seconds{quantile=&quot;0.25&quot;} 0.000156589<br>go_gc_duration_seconds{quantile=&quot;0.5&quot;} 0.000225361<br>go_gc_duration_seconds{quantile=&quot;0.75&quot;} 0.000313711<br>go_gc_duration_seconds{quantile=&quot;1&quot;} 0.001530204<br>go_gc_duration_seconds_sum 0.152570255<br>go_gc_duration_seconds_count 572<br># HELP go_goroutines Number of goroutines that currently exist.<br># TYPE go_goroutines gauge<br>go_goroutines 377<br># HELP go_info Information about the Go environment.</pre><p>These annotations can be applied either to the <strong>Service</strong> or the <strong>Pod/Deployment</strong>. However, adding them to the <strong>Service</strong> is the preferred approach, as it ensures a more stable and reliable configuration.</p><pre><br>tharindu@192 ~ % kubectl get services -n milvus-system<br>NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)              AGE<br>my-release-etcd            ClusterIP   34.118.229.67    &lt;none&gt;        2379/TCP,2380/TCP    4h2m<br>my-release-etcd-headless   ClusterIP   None             &lt;none&gt;        2379/TCP,2380/TCP    4h2m<br>my-release-milvus          ClusterIP   34.118.238.226   &lt;none&gt;        19530/TCP,9091/TCP   4h2m<br>my-release-minio           ClusterIP   34.118.233.162   &lt;none&gt;        9000/TCP             4h2m<br>tharindu@192 ~ % <br>tharindu@192 ~ % kubectl edit service my-release-milvus -n milvus-system</pre><p>Inside the <strong>metadata</strong> section of the service definition, add the following annotations:</p><pre>apiVersion: v1<br>kind: Service<br>metadata:<br>  name: my-release-milvus<br>  namespace: milvus-system<br>  annotations:<br>    prometheus.io/scrape: &quot;true&quot;<br>    prometheus.io/port: &quot;9091&quot;<br>    prometheus.io/path: &quot;/metrics&quot;<br>  labels:<br>    # existing labels...<br>spec:<br>  # existing spec...</pre><ul><li><strong>prometheus.io/scrape: &quot;true&quot;</strong> → Tells Prometheus to scrape metrics from this service.</li><li><strong>prometheus.io/path: &quot;/metrics&quot;</strong> → Defines the endpoint where Milvus exposes metrics.</li><li><strong>prometheus.io/port: &quot;9091&quot;</strong> → Specifies the port on which Milvus serves its metrics.</li></ul><p>Verification Steps After Adding Annotations:</p><pre># Check if Prometheus is scraping<br>kubectl port-forward svc/prometheus -n istio-system 9090:9090<br># Then visit localhost:9090 in browser<br># Go to Status -&gt; Targets<br># Look for endpoint with labels matching your Milvus service</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T19wLhio31uvxsR_t2dnJA.png" /><figcaption>Check Prometheus Targets on Web UI</figcaption></figure><h4>DDL Request Rate</h4><p>To monitor the rate of Data Definition Language (DDL) operations in Milvus:</p><pre>rate(milvus_rootcoord_ddl_req_count[5m])</pre><p>This query shows the per-second rate of DDL requests processed by Milvus’s root coordinator over 5-minute windows. It’s particularly useful for understanding the frequency of schema changes and collection management operations in the system.</p><p>We can go to Prometheus web UI Graph section and execute this command and check Graph.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7S6TkyI-WNgBEIwf46-WFw.png" /><figcaption>PQL Execution on Prometheus Web UI</figcaption></figure><h4>DDL Operations Status</h4><p>To track the success and failure of DDL operations:</p><pre>sum by (status) (milvus_rootcoord_ddl_req_count)</pre><p>This query aggregates DDL operations by their status, helping you monitor the reliability of your schema management operations. A sudden increase in failed operations could indicate configuration issues or resource constraints.</p><h4>Performance Monitoring</h4><h4>Request Latency Distribution</h4><p>To understand the performance characteristics of different Milvus operations:</p><pre>histogram_quantile(0.95,<br>  sum by (le, function_name) (rate(milvus_proxy_req_latency_bucket[5m]))<br>)</pre><p>This query shows the 95th percentile latency for different Milvus functions, meaning 95% of requests complete faster than this time. It’s crucial for:</p><ul><li>Identifying slow operations</li><li>Setting realistic SLAs</li><li>Spotting performance degradation early</li></ul><h4>Data Volume Metrics</h4><h4>Entity Count by Database</h4><pre>sum by (db_name) (milvus_rootcoord_entity_num{status=&quot;total&quot;})</pre><p>This query shows how many entities (vectors) are stored in each database, essential for capacity planning and understanding data distribution.</p><h4>Total Collections</h4><pre>sum(milvus_rootcoord_collection_num)</pre><p>Monitors the total number of collections across your Milvus instance, helping you track database growth and organization.</p><h4>Entity Count by Collection</h4><pre>sum by (collection_name) (milvus_rootcoord_entity_num{status=&quot;total&quot;})</pre><p>Breaks down entity counts at the collection level, useful for identifying which collections might need performance optimization or sharding.</p><h4>Resource Utilization</h4><h4>Memory Usage</h4><pre>round(container_memory_working_set_bytes{pod=~&quot;my-release-milvus.*&quot;, container!=&quot;POD&quot;} / 1024 / 1024, 0.1)</pre><p>This query shows memory usage in megabytes for each Milvus container, rounded to one decimal place for readability. It’s crucial for:</p><ul><li>Resource monitoring</li><li>Capacity planning</li><li>Preventing out-of-memory situations</li></ul><h4>CPU Usage</h4><pre>rate(container_cpu_usage_seconds_total{pod=~&quot;my-release-milvus.*&quot;, container!=&quot;POD&quot;}[5m]) * 1000</pre><p>Measures CPU utilization in millicores (where 1000 millicores = 1 CPU core), showing the 5-minute average CPU usage for each Milvus component. This helps you:</p><ul><li>Identify CPU-constrained components</li><li>Plan scaling decisions</li><li>Detect unusual CPU patterns</li></ul><h4>Storage Metrics</h4><h4>Binlog Size</h4><pre>sum(milvus_datacoord_stored_binlog_size)</pre><p>Tracks the total size of binary logs stored by Milvus, crucial for storage capacity planning.</p><h4>Index Size</h4><pre>sum(milvus_datacoord_stored_index_files_size)</pre><p>Shows the total size of index files, helping you understand the storage overhead of your search optimization structures.</p><h4>Milvus Grafana Dashboard</h4><p><strong>Port Forward Grafana: </strong>To access Grafana, first, port forward the Grafana service to your local machine:</p><pre>kubectl port-forward service/grafana 3000:3000 -n istio-system</pre><p>Replace &lt;namespace&gt; with the namespace where Grafana is deployed (e.g., monitoring or istio-system).</p><p><strong>Log in to the Web UI</strong></p><ol><li>Open your browser and go to <strong>http://localhost:3000</strong>.</li><li>Enter the default <strong>username</strong> and <strong>password</strong> (both are set to <strong>admin</strong> by default).</li><li>When prompted, set a new password for security.</li></ol><p>Once logged in, go to the <strong>Dashboard</strong> section. In the <strong>top-right corner</strong>, click on <strong>New</strong>, then select the <strong>Import</strong> option. After choosing the import option, select <strong>Import via JSON model</strong>. Copy and paste the JSON file into the provided field and click <strong>Load</strong>.</p><p>Once the dashboard is successfully loaded, it will populate all the metrics, including <strong>Prometheus Scrape</strong>, <strong>Milvus Instance</strong>, and other relevant data, using the specified <strong>PromQL</strong> queries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f-Pmj_6bSL_pzf7JTjhqag.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QgSLTqV9YwoDWRhCphwKqg.png" /></figure><p>Example dashboard</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mmIfcX7AZ_-YQgOQqDjoyg.png" /><figcaption>Grafana Dashboard (Import milvus-dashboard.json)</figcaption></figure><h3>Vector Storage Management System</h3><p>Before diving into the vector storage management system, it’s worth taking a closer look at the rationale behind the selection of our vector index parameters and distance metrics — decisions that are critical to optimizing recall without compromising too much on speed.</p><h4><strong>Distance Metric — cosine similarity</strong></h4><p>For my vector similarity search, I chose <strong>cosine similarity</strong> as the distance metric. Cosine similarity measures the angle between vectors, focusing on their directional alignment rather than their absolute magnitudes. This is particularly beneficial for text embeddings, where the semantic meaning is often best captured by the relative orientation of the vectors. In my context, cosine similarity enables the system to effectively discern subtle nuances in meaning, thereby ensuring that even the most contextually relevant chunks are identified.</p><h4>Indexing — <strong>HNSW (Hierarchical Navigable Small World)</strong></h4><p>On the indexing front, I employ an <strong>HNSW (Hierarchical Navigable Small World)</strong> index, which is well-regarded for its efficiency in high-dimensional nearest neighbor searches. Milvus supports several types of indexes — such as IVF_FLAT, IVF_PQ, ANNOY, and Flat indexes — but HNSW stands out due to its excellent balance between recall and speed. Its graph-based structure allows for rapid approximate searches, making it a suitable choice when the objective is to capture nearly all relevant results.</p><p>Our specific parameter choices for HNSW are also driven by the goal of maximizing recall:</p><ul><li><strong>M (set to 48):</strong> This parameter determines the number of bi-directional links each node has in the graph. A higher M value enhances the connectivity of the graph, which means that during the search process, more potential neighbors are considered. Although a higher M slightly increases the memory footprint and index construction time, it substantially improves the likelihood of retrieving all pertinent vectors.</li><li><strong>ef_construction (set to 400):</strong> During the index construction phase, ef_construction controls the size of the dynamic candidate list. A larger value here results in a more accurately connected graph, ensuring that the structure of the index is robust enough to support high recall. The trade-off is a longer build time, but this is acceptable given that the index is built offline.</li><li><strong>ef_search (set to 1000):</strong> This parameter specifies how many candidate neighbors are evaluated during each search query. A high ef_search value increases the depth of the search, which directly translates into higher recall. While this might marginally affect the query speed, it is a worthwhile compromise when the objective is to ensure that nearly all relevant text chunks are retrieved.</li></ul><p>By tuning these parameters — M at 48, ef_construction at 400, and ef_search at 1000 — I aim to optimize for the highest possible recall. The slight cost in terms of construction time and query speed is justified by the resulting comprehensive coverage of relevant chunks, a critical requirement when dealing with an extensive and nuanced corpus like The Wheel of Time series.</p><p>As the system scales, managing the large volume of document embeddings becomes a challenge in itself. The <strong>Vector Storage Management System</strong> addresses this by implementing an optimized approach for storing and querying embeddings. Utilizing an HNSW (Hierarchical Navigable Small World) index configured for cosine similarity, this module supports efficient similarity searches even across thousands of documents. It processes documents in batches — automatically tracking progress and adjusting batch sizes based on token usage estimates — to ensure that system constraints are respected. Detailed statistics and error reporting are maintained throughout the process, providing valuable insights for performance tuning and troubleshooting. The careful calibration of index parameters, such as the number of bi-directional links (M) and search parameters (ef_construction and ef_search), further enhances the retrieval performance.</p><pre>&quot;&quot;&quot;<br>Vector Storage Management System<br><br>This module provides a sophisticated system for managing vector storage operations in Milvus,<br>specifically optimized for large-scale document embedding storage and retrieval. The system<br>implements the HNSW (Hierarchical Navigable Small World) index with Cosine similarity for<br>efficient similarity search.<br><br>Key Features:<br>- Batch processing with automatic progress tracking<br>- Token usage estimation and monitoring<br>- Automatic batch size adjustment for token limits<br>- Detailed statistics and error reporting<br>- HNSW index optimization for text embeddings<br><br>The HNSW index is configured for optimal performance with text embeddings:<br>- M=48: Provides a balance between search speed and graph connectivity<br>- ef_construction=400: Ensures good graph quality during index building<br>- ef_search=1000: Enables deep search for high recall during query time<br><br>Author: Tharindu Sankalpa<br>Date: 2nd February 2025<br>&quot;&quot;&quot;<br><br>import os<br>from typing import List, Dict, Tuple, Any, Optional<br>from dataclasses import dataclass<br>from tqdm import tqdm<br>from langchain.docstore.document import Document<br>from pymilvus import Milvus<br><br><br>@dataclass<br>class ProcessingStats:<br>    &quot;&quot;&quot;<br>    Tracks statistics for document processing and vector storage operations.<br>    <br>    Attributes:<br>        total_documents: Total number of documents to process<br>        successful_documents: Number of successfully processed documents<br>        failed_documents: Number of documents that failed processing<br>        total_tokens: Total number of tokens processed<br>        batches_processed: Number of batches successfully processed<br>        failed_batches: List of dictionaries containing information about failed batches<br>    &quot;&quot;&quot;<br>    total_documents: int<br>    successful_documents: int = 0<br>    failed_documents: int = 0<br>    total_tokens: int = 0<br>    batches_processed: int = 0<br>    failed_batches: List[Dict[str, Any]] = None<br><br>    def __post_init__(self):<br>        self.failed_batches = []<br><br>    def add_failed_batch(self, batch_number: int, start_idx: int, end_idx: int, error: str):<br>        &quot;&quot;&quot;Records information about a failed batch.&quot;&quot;&quot;<br>        self.failed_batches.append({<br>            &quot;batch_number&quot;: batch_number,<br>            &quot;start_idx&quot;: start_idx,<br>            &quot;end_idx&quot;: end_idx,<br>            &quot;error&quot;: error<br>        })<br><br><br>class VectorStorageManager:<br>    &quot;&quot;&quot;<br>    Manages vector storage operations with optimized batch processing and monitoring.<br>    <br>    This class handles the complexities of storing document embeddings in Milvus,<br>    including batch processing, progress tracking, and error handling. It uses the<br>    HNSW index for efficient similarity search and implements automatic batch size<br>    adjustment to handle token limits.<br>    &quot;&quot;&quot;<br><br>    def __init__(<br>        self,<br>        uri: str = &quot;http://localhost:19530&quot;,<br>        user: Optional[str] = None,<br>        password: Optional[str] = None,<br>        db_name: str = &quot;default&quot;,<br>        collection_name: str = &quot;default_collection&quot;,<br>        embedding_function: Any = None,<br>    ):<br>        &quot;&quot;&quot;<br>        Initialize the vector storage manager with connection and index parameters.<br><br>        The manager uses HNSW indexing optimized for text embeddings. The parameters are<br>        chosen to balance search speed, accuracy, and memory usage:<br>        - M=48: Higher than default for better connectivity<br>        - ef_construction=400: Increased for better index quality<br>        - ef_search=1000: High value for thorough search<br><br>        Args:<br>            uri: Milvus server URI<br>            user: Username for authentication (optional)<br>            password: Password for authentication (optional)<br>            db_name: Target database name<br>            collection_name: Name of the collection to store vectors<br>            embedding_function: Function to generate embeddings<br>        &quot;&quot;&quot;<br>        self.uri = uri<br>        self.user = user or os.getenv(&quot;MILVUS_USER&quot;)<br>        self.password = password or os.getenv(&quot;MILVUS_PASSWORD&quot;)<br>        self.db_name = db_name<br>        self.collection_name = collection_name<br>        self.embedding_function = embedding_function<br>        <br>        self.index_params = {<br>            &quot;metric_type&quot;: &quot;COSINE&quot;,<br>            &quot;index_type&quot;: &quot;HNSW&quot;,<br>            &quot;params&quot;: {<br>                &quot;M&quot;: 48,                  # Number of bi-directional links<br>                &quot;ef_construction&quot;: 400,    # Search scope during building<br>                &quot;ef_search&quot;: 1000         # Search scope during searching<br>            }<br>        }<br>        <br>        self.vector_store = None<br>        self._initialize_store()<br><br>    def _initialize_store(self):<br>        &quot;&quot;&quot;<br>        Initialize the Milvus vector store with optimized settings.<br>        <br>        This method sets up the vector store with HNSW indexing and proper<br>        authentication. It handles the connection setup and index configuration.<br>        &quot;&quot;&quot;<br>        try:<br>            self.vector_store = Milvus(<br>                embedding_function=self.embedding_function,<br>                connection_args={<br>                    &quot;uri&quot;: self.uri,<br>                    &quot;user&quot;: self.user,<br>                    &quot;password&quot;: self.password,<br>                    &quot;db_name&quot;: self.db_name<br>                },<br>                collection_name=self.collection_name,<br>                index_params=self.index_params,<br>                auto_id=True<br>            )<br>        except Exception as e:<br>            print(f&quot;Failed to initialize vector store: {str(e)}&quot;)<br>            raise<br><br>    @staticmethod<br>    def calculate_tokens(texts: List[str]) -&gt; int:<br>        &quot;&quot;&quot;<br>        Estimate the token count for a list of texts.<br>        <br>        This method uses a simple estimation based on character count, assuming<br>        approximately 4 characters per token (OpenAI&#39;s typical ratio). While not<br>        exact, this provides a useful approximation for batch size management.<br><br>        Args:<br>            texts: List of text strings to estimate tokens for<br><br>        Returns:<br>            Estimated total number of tokens<br>        &quot;&quot;&quot;<br>        return sum(len(text) // 4 for text in texts)<br><br>    def add_documents(<br>        self,<br>        documents: List[Document],<br>        batch_size: int = 100<br>    ) -&gt; Tuple[bool, ProcessingStats]:<br>        &quot;&quot;&quot;<br>        Add documents to the vector store with sophisticated batch processing.<br><br>        This method implements a robust batch processing system with:<br>        - Progress tracking using tqdm<br>        - Automatic batch size adjustment<br>        - Detailed statistics collection<br>        - Error handling and reporting<br>        <br>        The batch processing includes automatic retry with smaller batch sizes<br>        if token limits are exceeded, ensuring maximum throughput while staying<br>        within system constraints.<br><br>        Args:<br>            documents: List of Document objects to process<br>            batch_size: Initial batch size (will be adjusted if needed)<br><br>        Returns:<br>            Tuple of (success_flag, processing_statistics)<br>        <br>        Example:<br>            manager = VectorStorageManager(...)<br>            docs = [Document(...), Document(...)]<br>            success, stats = manager.add_documents(docs)<br>            if success:<br>                print(f&quot;Processed {stats.successful_documents} documents&quot;)<br>            else:<br>                print(&quot;Some batches failed:&quot;, stats.failed_batches)<br>        &quot;&quot;&quot;<br>        total_docs = len(documents)<br>        stats = ProcessingStats(total_documents=total_docs)<br>        current_batch_size = batch_size<br><br>        # Create progress bar<br>        with tqdm(total=total_docs, desc=&quot;Processing documents&quot;) as progress:<br>            for i in range(0, total_docs, current_batch_size):<br>                end_idx = min(i + current_batch_size, total_docs)<br>                batch = documents[i:end_idx]<br>                batch_texts = [doc.page_content for doc in batch]<br>                estimated_tokens = self.calculate_tokens(batch_texts)<br>                <br>                try:<br>                    print(f&quot;\nProcessing batch {i//current_batch_size + 1}&quot;)<br>                    print(f&quot;Estimated tokens in batch: {estimated_tokens}&quot;)<br>                    print(f&quot;Documents {i} to {end_idx} of {total_docs}&quot;)<br>                    <br>                    self.vector_store.add_documents(batch)<br>                    <br>                    # Update statistics<br>                    stats.successful_documents += len(batch)<br>                    stats.total_tokens += estimated_tokens<br>                    stats.batches_processed += 1<br>                    progress.update(len(batch))<br>                    <br>                except Exception as e:<br>                    error_msg = str(e)<br>                    print(f&quot;\nError processing batch {i//current_batch_size + 1}: {error_msg}&quot;)<br>                    <br>                    stats.add_failed_batch(<br>                        batch_number=i//current_batch_size + 1,<br>                        start_idx=i,<br>                        end_idx=end_idx,<br>                        error=error_msg<br>                    )<br>                    stats.failed_documents += len(batch)<br>                    <br>                    # Adjust batch size if needed<br>                    if &quot;max_tokens_per_request&quot; in error_msg:<br>                        current_batch_size = current_batch_size // 2<br>                        print(f&quot;Token limit exceeded. Reducing batch size to {current_batch_size}&quot;)<br>        <br>        # Print final statistics<br>        self._print_processing_summary(stats)<br>        <br>        return len(stats.failed_batches) == 0, stats<br><br>    def _print_processing_summary(self, stats: ProcessingStats):<br>        &quot;&quot;&quot;<br>        Print a detailed summary of the processing statistics.<br>        <br>        Args:<br>            stats: ProcessingStats object containing the processing results<br>        &quot;&quot;&quot;<br>        print(&quot;\nProcessing Complete!&quot;)<br>        print(f&quot;Total documents processed: {stats.successful_documents}/{stats.total_documents}&quot;)<br>        print(f&quot;Total tokens processed: {stats.total_tokens}&quot;)<br>        <br>        if stats.successful_documents &gt; 0:<br>            avg_tokens = stats.total_tokens / stats.successful_documents<br>            print(f&quot;Average tokens per document: {avg_tokens:.2f}&quot;)<br>            <br>        print(f&quot;Total batches processed: {stats.batches_processed}&quot;)<br>        print(f&quot;Failed documents: {stats.failed_documents}&quot;)<br>        <br>        if stats.failed_batches:<br>            print(&quot;\nFailed batches:&quot;, stats.failed_batches)<br><br><br># Example usage<br>if __name__ == &quot;__main__&quot;:<br>    # Initialize the manager<br>    manager = VectorStorageManager(<br>        db_name=&quot;wheel_of_time_db&quot;,<br>        collection_name=&quot;wheeloftime_hnsw_cosine&quot;,<br>        embedding_function=your_embedding_function<br>    )<br>    <br>    # Process documents<br>    success, stats = manager.add_documents(your_documents, batch_size=100)<br>    <br>    if success:<br>        print(&quot;All documents processed successfully&quot;)<br>    else:<br>        print(&quot;Some batches failed, check the statistics for details&quot;)</pre><h3>Wheel of Time Retrieval Augmented Generation (RAG) System</h3><p>This part of the architecture is responsible for <strong>handling user queries and performing similarity search</strong>.</p><ul><li><strong>Query Processing: </strong>Converts user queries into vector embeddings using a suitable embedding function (e.g., OpenAI embeddings).</li><li><strong>Contextual Retrieval: </strong>Performs similarity search against the Milvus vector store to retrieve the top-K most relevant text chunks.</li><li><strong>Metadata Filtering: </strong>Applies metadata-based filters (such as book title and chapter range) to narrow down search results.</li><li><strong>Response Generation: </strong>Combines retrieved context with the original query to generate precise, context-aware responses.</li><li><strong>Advanced Query Features: </strong>Supports cross-book searches, chapter-specific queries, and combined semantic and metadata filtering.</li><li><strong>Result Formatting: </strong>Provides clearly formatted search results with similarity scores, content previews, and associated metadata, enhancing the overall user experience.</li></ul><pre>&quot;&quot;&quot;<br>Wheel of Time Retrieval Augmented Generation (RAG) System<br><br>This module implements a sophisticated RAG system specifically designed for the Wheel of Time<br>series. RAG systems enhance language model responses by retrieving relevant context from a<br>document collection before generating answers. This implementation combines vector similarity<br>search with metadata filtering to provide precise and contextually relevant responses.<br><br>The system works through several steps:<br>1. Query Processing: Convert user questions into vector embeddings<br>2. Context Retrieval: Find relevant passages using similarity search and filters<br>3. Response Generation: Combine retrieved context with the original query<br><br>Key Features:<br>- Semantic similarity search using HNSW index<br>- Metadata-based filtering (book, chapter, etc.)<br>- Configurable search parameters<br>- Detailed result formatting and scoring<br><br>The system uses Milvus as its vector store and supports advanced query features like:<br>- Cross-book searches<br>- Chapter-specific queries<br>- Combined metadata and semantic filtering<br><br>Author: Tharindu Sankalpa<br>Date: 2nd February 2025<br>&quot;&quot;&quot;<br><br>import os<br>from typing import List, Dict, Tuple, Optional, Any<br>from dataclasses import dataclass<br>from langchain.docstore.document import Document<br>from langchain_milvus import Milvus<br><br>@dataclass<br>class SearchResult:<br>    &quot;&quot;&quot;<br>    Represents a single search result from the RAG system.<br>    <br>    Attributes:<br>        content: The retrieved text content<br>        similarity_score: Cosine similarity score with the query<br>        metadata: Document metadata (book, chapter, etc.)<br>        content_preview: Truncated content for display<br>    &quot;&quot;&quot;<br>    content: str<br>    similarity_score: float<br>    metadata: Dict[str, Any]<br>    content_preview: str<br><br>    @property<br>    def formatted_output(self) -&gt; str:<br>        &quot;&quot;&quot;Creates a formatted string representation of the search result.&quot;&quot;&quot;<br>        return f&quot;&quot;&quot;<br>Similarity Score: {self.similarity_score:.4f}<br>Book: {self.metadata.get(&#39;book_name&#39;, &#39;N/A&#39;)}<br>Chapter: {self.metadata.get(&#39;chapter_title&#39;, &#39;N/A&#39;)} (Number: {self.metadata.get(&#39;chapter_number&#39;, &#39;N/A&#39;)})<br>Content Preview: {self.content_preview}<br>{&#39;-&#39; * 80}&quot;&quot;&quot;<br><br>class WheelOfTimeRAG:<br>    &quot;&quot;&quot;<br>    A comprehensive RAG system for the Wheel of Time series that combines<br>    semantic search with metadata filtering for precise information retrieval.<br>    <br>    This class provides a sophisticated interface for querying the Wheel of Time<br>    corpus, allowing users to find specific information while maintaining the<br>    context and narrative structure of the books.<br>    &quot;&quot;&quot;<br><br>    def __init__(<br>        self,<br>        uri: str = &quot;http://localhost:19530&quot;,<br>        user: Optional[str] = None,<br>        password: Optional[str] = None,<br>        db_name: str = &quot;wheel_of_time_db&quot;,<br>        collection_name: str = &quot;wheeloftime_hnsw_cosine&quot;,<br>        embedding_function: Any = None,<br>        preview_length: int = 200<br>    ):<br>        &quot;&quot;&quot;<br>        Initialize the RAG system with connection and search parameters.<br><br>        The system connects to a Milvus vector store containing pre-processed<br>        text chunks from the Wheel of Time series. Each chunk is enriched with<br>        metadata about its source book, chapter, and position in the narrative.<br><br>        Args:<br>            uri: Milvus server URI<br>            user: Username for authentication<br>            password: Password for authentication<br>            db_name: Target database name<br>            collection_name: Name of the vector collection<br>            embedding_function: Function to generate query embeddings<br>            preview_length: Length of content previews in results<br>        &quot;&quot;&quot;<br>        self.uri = uri<br>        self.user = user or os.getenv(&quot;MILVUS_USER&quot;)<br>        self.password = password or os.getenv(&quot;MILVUS_PASSWORD&quot;)<br>        self.db_name = db_name<br>        self.collection_name = collection_name<br>        self.embedding_function = embedding_function<br>        self.preview_length = preview_length<br>        <br>        self.vector_store = self._initialize_store()<br><br>    def _initialize_store(self) -&gt; Milvus:<br>        &quot;&quot;&quot;<br>        Initialize connection to the Milvus vector store.<br>        <br>        Returns:<br>            Configured Milvus client ready for querying<br>        &quot;&quot;&quot;<br>        try:<br>            return Milvus(<br>                embedding_function=self.embedding_function,<br>                connection_args={<br>                    &quot;uri&quot;: self.uri,<br>                    &quot;user&quot;: self.user,<br>                    &quot;password&quot;: self.password,<br>                    &quot;db_name&quot;: self.db_name<br>                },<br>                collection_name=self.collection_name<br>            )<br>        except Exception as e:<br>            print(f&quot;Failed to initialize vector store: {str(e)}&quot;)<br>            raise<br><br>    def search(<br>        self,<br>        query: str,<br>        k: int = 10,<br>        book_filter: Optional[str] = None,<br>        chapter_range: Optional[Tuple[int, int]] = None,<br>        additional_filters: Optional[str] = None<br>    ) -&gt; List[SearchResult]:<br>        &quot;&quot;&quot;<br>        Perform a semantic search with optional metadata filtering.<br><br>        This method combines vector similarity search with metadata filters to<br>        find the most relevant passages. It supports various filtering options<br>        to narrow down the search scope:<br><br>        - Book-specific search<br>        - Chapter range filtering<br>        - Custom metadata filters<br>        <br>        The results are ordered by similarity score and include both the content<br>        and its metadata context.<br><br>        Args:<br>            query: The search query text<br>            k: Number of results to return<br>            book_filter: Specific book to search within<br>            chapter_range: Tuple of (start_chapter, end_chapter) to search within<br>            additional_filters: Additional Milvus expression filters<br><br>        Returns:<br>            List of SearchResult objects containing the matches and their metadata<br><br>        Example:<br>            rag = WheelOfTimeRAG()<br>            results = rag.search(<br>                query=&quot;When does Rand first channel?&quot;,<br>                book_filter=&quot;The Eye of the World&quot;,<br>                chapter_range=(1, 15)<br>            )<br>            for result in results:<br>                print(result.formatted_output)<br>        &quot;&quot;&quot;<br>        # Build combined filter expression<br>        filters = []<br>        if book_filter:<br>            filters.append(f&quot;book_name like &#39;{book_filter}&#39;&quot;)<br>        if chapter_range:<br>            start, end = chapter_range<br>            filters.append(f&quot;chapter_number &gt;= {start} and chapter_number &lt;= {end}&quot;)<br>        if additional_filters:<br>            filters.append(additional_filters)<br>        <br>        combined_filter = &quot; and &quot;.join(filters) if filters else None<br><br>        # Perform search<br>        results = self.vector_store.similarity_search_with_score(<br>            query=query,<br>            k=k,<br>            expr=combined_filter<br>        )<br><br>        # Process results<br>        return [<br>            SearchResult(<br>                content=doc.page_content,<br>                similarity_score=score,<br>                metadata=doc.metadata,<br>                content_preview=doc.page_content[:self.preview_length] + &quot;...&quot;<br>            )<br>            for doc, score in results<br>        ]<br><br>    def get_book_context(<br>        self,<br>        query: str,<br>        book_title: str,<br>        k: int = 5<br>    ) -&gt; List[SearchResult]:<br>        &quot;&quot;&quot;<br>        Search within a specific book context.<br><br>        This is a convenience method for book-specific searches that handles<br>        the proper filtering automatically.<br><br>        Args:<br>            query: Search query<br>            book_title: Title of the book to search within<br>            k: Number of results to return<br><br>        Returns:<br>            List of search results from the specified book<br>        &quot;&quot;&quot;<br>        return self.search(<br>            query=query,<br>            k=k,<br>            book_filter=book_title<br>        )<br><br>    def get_chapter_context(<br>        self,<br>        query: str,<br>        book_title: str,<br>        chapter: int,<br>        context_window: int = 2,<br>        k: int = 5<br>    ) -&gt; List[SearchResult]:<br>        &quot;&quot;&quot;<br>        Search within a specific chapter and its surrounding context.<br><br>        This method searches within a chapter and its neighboring chapters to<br>        capture relevant context that might span chapter boundaries.<br><br>        Args:<br>            query: Search query<br>            book_title: Book to search within<br>            chapter: Target chapter number<br>            context_window: Number of chapters to include before and after<br>            k: Number of results to return<br><br>        Returns:<br>            List of search results from the chapter and its context<br>        &quot;&quot;&quot;<br>        start_chapter = max(1, chapter - context_window)<br>        end_chapter = chapter + context_window<br>        <br>        return self.search(<br>            query=query,<br>            k=k,<br>            book_filter=book_title,<br>            chapter_range=(start_chapter, end_chapter)<br>        )<br><br># Example usage<br>if __name__ == &quot;__main__&quot;:<br><br>    # Initialize RAG system<br>    rag = WheelOfTimeRAG(<br>            embedding_function=embeddings<br>        )<br>        <br>    # Example search<br>    results = rag.search(<br>            query=&quot;What is the exact moment when Rand first realizes that Moiraine is an Aes Sedai?&quot;,<br>            book_filter=&quot;01. The Eye of the World&quot;<br>        )<br><br>    # Display results<br>    for result in results:<br>        print(result.formatted_output)</pre><pre><br>Similarity Score: 0.6137<br>Book: 01. The Eye of the World<br>Chapter: Out of the Woods (Number: 7)<br>Content Preview: Rand stared at her, unable to speak. An Aes Sedai. He had been trying to convince himself that she would not look any different now that he knew who . . . what he was looking at, and to his surprise s...<br>--------------------------------------------------------------------------------<br><br>Similarity Score: 0.6026<br>Book: 01. The Eye of the World<br>Chapter: Listen to the Wind (Number: 21)<br>Content Preview: “Mistress Barran taught me well.” She tried looking at Lan, but his eyes still made her uncomfortable, so she settled for staring over the Aes Sedai’s head at the river. How dare the village gossip in...<br>--------------------------------------------------------------------------------<br><br>Similarity Score: 0.6003<br>Book: 01. The Eye of the World<br>Chapter: Strangers (Number: 2)<br>Content Preview: Rand stared at the bird in consternation. “Did you ever see a raven do that?” he asked quietly.<br><br>Mat shook his head without looking away from the raven. “Never. Nor any other bird, either.”<br><br>“A vile b...<br>--------------------------------------------------------------------------------<br><br>Similarity Score: 0.5941<br>Book: 01. The Eye of the World<br>Chapter: Strangers and Friends (Number: 15)<br>Content Preview: “She didn’t say she’s a soothsayer,” Rand said. “I believe she does see things. Remember, Moiraine was talking to her when we finished our baths. And she knows who Moiraine is.”<br><br>Mat frowned at him. “...<br>--------------------------------------------------------------------------------<br><br>Similarity Score: 0.5929<br>Book: 01. The Eye of the World<br>Chapter: A Place of Safety (Number: 8)<br>Content Preview: The fierceness of her eyes, or the calm of her voice—not gentle, exactly; more firmly in command—Rand did not know which to believe. Or perhaps they did go together. Aes Sedai. He was committed, now. ...<br>--------------------------------------------------------------------------------<br><br>Similarity Score: 0.5921<br>Book: 01. The Eye of the World<br>Chapter: Listen to the Wind (Number: 21)<br>Content Preview: Nynaeve pushed at Lan’s arm again; it still did not move, and she decided to ignore it. “Why don’t you try claiming I am a Trolloc?”<br><br>Moiraine’s smile was so knowing that Nynaeve wanted to hit her. “D...<br>--------------------------------------------------------------------------------<br><br>Similarity Score: 0.5888<br>Book: 01. The Eye of the World<br>Chapter: Out of the Woods (Number: 7)<br>Content Preview: Rand was too tired, too stunned, to speak. All through the night Emond’s Field had been a haven, where he and Tam would be safe. Now all he could seem to do was stare in dismay at her smoke-stained dr...<br>--------------------------------------------------------------------------------<br><br>Similarity Score: 0.5875<br>Book: 01. The Eye of the World<br>Chapter: Choices (Number: 13)<br>Content Preview: He snorted. “As soon as you reach Tar Valon. Why? Light, tell me that. You’re no Darkfriend.”<br><br>“Do you think Moiraine Sedai is a Darkfriend? Do you?” She squared around to face him with her fists clen...<br>--------------------------------------------------------------------------------<br><br>Similarity Score: 0.5874<br>Book: 01. The Eye of the World<br>Chapter: Choices (Number: 13)<br>Content Preview: “You mean . . . ?” With a cry of delight Egwene threw her arms around the Aes Sedai. “Oh, thank you. Rand, did you hear? I’m going to be an Aes Sedai!”</pre><p>When I queried the system with:</p><pre>results = rag.search(<br>    query=&quot;What is the exact moment when Rand first realizes that Moiraine is an Aes Sedai?&quot;,<br>    book_filter=&quot;01. The Eye of the World&quot;<br>)</pre><p>I received several results with closely clustered similarity scores. Based on my careful reading of <em>The Eye of the World</em>, I know that the exact, explicit moment of realization is found in the chapter <strong>“Out of the Woods” (Chapter 7)</strong>. This is where Rand is confronted with the undeniable evidence of Moiraine’s true nature as an Aes Sedai — an insight clearly captured in the retrieved excerpt:</p><blockquote><strong><em>Similarity Score: 0.6137</em></strong><em><br></em><strong><em>Book:</em></strong><em> 01. The Eye of the World<br></em><strong><em>Chapter:</em></strong><em> Out of the Woods (Number: 7)<br></em><strong><em>Content Preview:</em></strong><em><br></em>Rand stared at her, unable to speak. An Aes Sedai. He had been trying to convince himself that she would not look any different now that he knew who . . . what he was looking at, and to his surprise s…</blockquote><p>This result directly aligns with my understanding of the narrative. The explicit moment is well represented, which confirms that the tuning of the HNSW parameters in the vector store is performing as intended — accurately capturing even subtle shifts in narrative tone and context.</p><p>Moreover, the system also returned excerpts from other chapters:</p><ul><li><strong>Chapter 2 (“Strangers”)</strong>:<br>Although this chapter does not contain the explicit declaration, it marks the first hints of suspicion when Moiraine and Lan arrive at the Two Rivers. Rand’s reaction — his subtle observations and the uneasy feeling evoked by her appearance — suggests that his intuition was already beginning to stir. This indirect cue is critical because it shows that the system is sensitive not only to clear-cut moments but also to the progressive buildup of narrative insight.</li><li>Additional results from <strong>Chapter 21 (“Listen to the Wind”)</strong> and <strong>Chapter 15 (“Strangers and Friends”)</strong> further reinforce this context by capturing moments that contribute to the evolving nature of Rand’s perceptions.</li></ul><p>The fact that the search results include both the explicit moment (Chapter 7) and these earlier, more nuanced hints (Chapter 2, and others) demonstrates that the semantic search is effectively tuned. The similarity scores — ranging from 0.6137 to 0.5941 — indicate a consistent matching process that respects the subtle gradations of narrative context.</p><p>Once this carefully curated context is fed into a large language model, it can generate a highly insightful answer that not only identifies the explicit moment of realization but also weaves in the broader narrative cues. This capability is a testament to the precision and effectiveness of the tuning and processing methodologies applied in the system.</p><p>In summary, the final results justify that:</p><ul><li><strong>The explicit realization in “Out of the Woods” (Chapter 7) is correctly identified.</strong></li><li><strong>Subtle, context-building cues from earlier chapters are also captured, offering a richer, more comprehensive view of Rand’s evolving understanding.</strong></li></ul><p>This success validates the entire approach and paves the way for further exploration — such as integrating LangChain and LangGraph workflows with generative AI agents — which I will delve into in future articles.</p><h3>Conclusion</h3><p>Building this scalable RAG system for <em>The Wheel of Time</em> has been both an enlightening and challenging journey. Through this project, I have demonstrated how combining Milvus for vector indexing, Kubernetes for orchestration, Istio for service management, and a robust observability stack can result in a system that is not only efficient and scalable but also capable of handling highly complex datasets.</p><p>This article has covered:</p><ul><li><strong>The Fusion of Generative AI and Retrieval:</strong> How RAG systems can deliver context-rich, precise answers by marrying large language models with semantic search.</li><li><strong>The Value of a Thoughtful Tech Stack:</strong> My rationale for choosing technologies like Milvus, Kubernetes, and Istio — and how each plays a pivotal role in ensuring system performance and reliability.</li><li><strong>Practical Implementation Details:</strong> Step-by-step insights into environment setup, data processing, deployment, and monitoring, along with lessons learned from overcoming real-world challenges.</li></ul><p>Looking ahead, I plan to further extend this work by exploring how to generate answer-driven responses using <strong>LangChain</strong> and <strong>LangGraph</strong> workflows, incorporating advanced <strong>generative AI agents</strong> to push the boundaries even further. I hope this article serves as a valuable resource and inspiration for anyone looking to harness the power of modern AI-driven search systems.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d04f7e315cfd" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>