<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Offisong Emmanuel</title>
    <description>The latest articles on DEV Community by Offisong Emmanuel (@offisong_emmanuel_c6bc311).</description>
    <link>https://dev.to/offisong_emmanuel_c6bc311</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3711383%2F9a0446ad-7e5d-47f6-89b8-e8c7001a9aa5.jpeg</url>
      <title>DEV Community: Offisong Emmanuel</title>
      <link>https://dev.to/offisong_emmanuel_c6bc311</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/offisong_emmanuel_c6bc311"/>
    <language>en</language>
    <item>
      <title>What You Need to Know About AI Agent Memory Architecture</title>
      <dc:creator>Offisong Emmanuel</dc:creator>
      <pubDate>Thu, 08 Oct 2026 18:01:34 +0000</pubDate>
      <link>https://dev.to/actiandev/what-you-need-to-know-about-ai-agent-memory-architecture-49ek</link>
      <guid>https://dev.to/actiandev/what-you-need-to-know-about-ai-agent-memory-architecture-49ek</guid>
      <description>&lt;p&gt;AI agent memory architecture is what separates a production-ready agent from one that forgets everything after every interaction. &lt;/p&gt;

&lt;p&gt;Many developers discover the limits of stateless agents the hard way. An agent may perform well during a single conversation because everything fits inside the context window. The moment a new session starts, it loses user preferences, repeats past mistakes, and behaves as though every interaction is the first. Adding a vector database alone does not solve this problem. A reliable memory layer requires clear decisions about what information to store, when to store it, how to organize it, and how to retrieve it at the right time.&lt;/p&gt;

&lt;p&gt;This article explains how modern AI agent memory systems work and how the different memory components fit together. You will learn the memory types, architecture patterns used in production agents, and the lifecycles that keep memory accurate over time. You will also see why concepts such as context windows, vector similarity search, retrieval augmented generation (RAG), knowledge graphs, and persistent memory each play a different role within the overall architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Agent memory architecture determines what your agent remembers, how long it retains information, and how it retrieves it.
&lt;/li&gt;
&lt;li&gt;Four core memory types support different needs: working, episodic, semantic, and procedural memory.
&lt;/li&gt;
&lt;li&gt;Five architecture patterns range from working-memory-only systems to enterprise context layers.
&lt;/li&gt;
&lt;li&gt;Start with episodic memory and a flat external vector store for a simple path to cross-session continuity.
&lt;/li&gt;
&lt;li&gt;Define clear write triggers, manage memory over time, and monitor retrieval quality to keep your agent's memory useful and reliable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Stateless Agents Fail in Production&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Stateless agents fail in production because they forget everything outside the current interaction. Without a memory architecture, your agent cannot maintain cross-session continuity, learn from previous outcomes, or adapt to users over time. It may produce impressive results in demos, but it behaves like a new system every time a request arrives.&lt;/p&gt;

&lt;p&gt;Most LLM applications are stateless by default. Every API request starts with a fresh context window, and the model only knows what you include in the prompt. Once the interaction ends, that context disappears. If a user returns tomorrow, the agent has no record of previous conversations, completed tasks, or established preferences unless you explicitly retrieve and provide that information again.&lt;/p&gt;

&lt;p&gt;This limitation creates three common failure modes in production systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Loss of cross-session continuity&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A stateless agent cannot carry information from one session to the next. Once the context window resets, every previous conversation, decision, and interaction disappears unless your application explicitly retrieves it from an external memory system.&lt;/p&gt;

&lt;p&gt;This becomes obvious in production-grade LLM applications. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; A customer support agent asks users to verify their account details every time they return because it has no memory of previous interactions.
&lt;/li&gt;
&lt;li&gt;A coding assistant forgets project conventions, preferred libraries, or architectural decisions made earlier.
&lt;/li&gt;
&lt;li&gt;A research agent loses track of completed work and starts gathering the same information again instead of continuing where it left off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cross-session continuity prevents an LLM application from starting over with every request.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Inability to learn From past outcomes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Without persistent memory, your agent cannot improve through experience. It treats every problem as a new problem because it has no record of what succeeded or failed before.&lt;/p&gt;

&lt;p&gt;For example, an operations agent troubleshooting infrastructure incidents may recommend the same failed fix repeatedly because it never stored the outcome of previous incidents. Over time, this creates frustration because the agent is incapable of learning despite having access to powerful language models.&lt;/p&gt;

&lt;p&gt;Production agents should capture high-value events such as confirmed fixes, accepted recommendations, and explicit user corrections. Those records become the foundation for better decisions in future interactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Lack of personalization&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Users expect production AI agents to adapt over time. Instead of treating every interaction the same, the agent should remember individual preferences, communication styles, recurring workflows, and frequently used tools.&lt;/p&gt;

&lt;p&gt;Consider a writing assistant that knows you prefer concise responses or a DevOps assistant that remembers your Kubernetes deployment standards. That knowledge allows the agent to produce more relevant responses without asking the same questions repeatedly. Without persistent memory, every recommendation remains generic because the agent has no accumulated understanding of the user or the environment.&lt;/p&gt;

&lt;p&gt;Many teams try to solve these problems by attaching a vector database to the application. That improves retrieval, but it does not solve the architectural problem on its own. An effective AI agent memory architecture defines what information deserves long-term storage, when it should move from short-term to persistent memory, how it should be organized, and which retrieval strategy should surface at the right moment. Those decisions determine whether your agent becomes more capable over time or simply stores large amounts of information it cannot use effectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Four Types of Agent Memory&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AI agent memory is not a single storage layer. It consists of four memory types that work together to help your agent reason, remember, and improve over time. Each serves a different purpose, has a different lifespan, and uses a different storage and retrieval mechanism. Understanding these memory types is the first step toward designing an effective AI agent memory architecture. &lt;/p&gt;

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

&lt;p&gt;Figure 1. The four types of agent memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Working memory&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Working memory stores the information your agent needs during the current interaction. In most AI agents, this is the context window that contains the active conversation, intermediate reasoning, tool outputs, and temporary variables.&lt;/p&gt;

&lt;p&gt;Working memory is fast because it lives alongside the model during inference, but it is also temporary. Once the session ends, the contents disappear unless your application explicitly saves important information elsewhere. Increasing the context window gives the model access to more information during a single request, &lt;a href="https://mem0.ai/blog/context-window-vs-persistent-memory-why-1m-tokens-isn-t-enough" rel="noopener noreferrer"&gt;but it does not create persistent memory across sessions&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Use working memory for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current conversation history.
&lt;/li&gt;
&lt;li&gt;Active reasoning and planning.
&lt;/li&gt;
&lt;li&gt;Temporary tool outputs.
&lt;/li&gt;
&lt;li&gt;Session-specific state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Episodic memory&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Episodic memory stores specific events and interactions that happened in the past. Instead of remembering everything, it captures experiences that may become useful later.&lt;/p&gt;

&lt;p&gt;For example, an AI customer support agent may remember that a customer declined a refund and requested a replacement last week. A coding assistant may remember that you rejected a suggested implementation because it violated your project's architecture. Unlike working memory, episodic memory survives across sessions.&lt;/p&gt;

&lt;p&gt;Most production systems store episodic memory in a vector database. The system converts each memory into an embedding and retrieves it through vector similarity search. This allows the agent to recall relevant past experiences instead of replaying every previous conversation. For on-premises or edge deployment, you will need vector databases that stay within your company network. &lt;a href="https://dev.to/actiandev/how-to-build-persistent-agent-memory-across-sessions-117"&gt;Actian VectorAI DB provides persistent vector storage&lt;/a&gt; while keeping your data local.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Semantic memory&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://redis.io/blog/semantic-memory-search-ai-agents/" rel="noopener noreferrer"&gt;Semantic memory&lt;/a&gt; stores generalized knowledge that the agent learns over time. Instead of remembering individual events, it extracts stable facts and patterns from repeated experiences.&lt;/p&gt;

&lt;p&gt;For example, after observing several accepted responses, the agent may learn that a user prefers concise explanations or always deploys applications with Kubernetes. Rather than storing every interaction separately, the system consolidates those experiences into durable knowledge that the agent can reuse in future conversations.&lt;/p&gt;

&lt;p&gt;You can store semantic memory as embeddings, structured records, or a combination of both. Many production systems periodically promote important information from &lt;a href="https://mem0.ai/blog/state-of-ai-agent-memory-2026" rel="noopener noreferrer"&gt;episodic memory into semantic memory&lt;/a&gt; to reduce duplication and improve retrieval quality. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Procedural memory&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://mem0.ai/blog/procedural-memory-explained-teaching-ai-agents-how-to-perform-tasks" rel="noopener noreferrer"&gt;Procedural memory&lt;/a&gt; stores knowledge about how the agent performs tasks. Instead of remembering facts about users or conversations, it remembers workflows, skills, and execution logic.&lt;/p&gt;

&lt;p&gt;Examples include tool definitions, workflow graphs, prompt templates, function-calling policies, approval rules, and orchestration logic. When your agent executes a multi-step workflow or invokes external tools, it relies on procedural memory rather than episodic or semantic memory.&lt;/p&gt;

&lt;p&gt;Unlike the other memory types, procedural memory usually lives in code, configuration files, workflow definitions, or orchestration graphs. It changes only when you update the agent's capabilities or improve its workflows, making it the most stable layer of the architecture.&lt;/p&gt;

&lt;p&gt;Together, these four memory types form a complete AI agent memory architecture. Working memory supports the current task, episodic memory preserves experiences, semantic memory captures long-term knowledge, and procedural memory defines how the agent performs work. Separating these responsibilities allows your agent to scale beyond a single context window and maintain reliable behavior across long-running interactions. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Five Architecture Patterns&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;No single AI agent memory architecture works for every application. The right pattern depends on how much context your agent needs to retain, how quickly it must retrieve information, and where your data is allowed to reside. Most production systems today fall into one of five architecture patterns, each representing a different tradeoff between simplicity, capability, and operational complexity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Best Use Case&lt;/th&gt;
&lt;th&gt;Primary Tradeoff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In-process (working only)&lt;/td&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;td&gt;Single-session assistants, one-off tasks&lt;/td&gt;
&lt;td&gt;No persistence across sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat external vector store&lt;/td&gt;
&lt;td&gt;Vector database&lt;/td&gt;
&lt;td&gt;Cross-session recall with minimal complexity&lt;/td&gt;
&lt;td&gt;Retrieval quality degrades as memory grows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiered memory (episodic + semantic)&lt;/td&gt;
&lt;td&gt;Separate vector stores or indexes&lt;/td&gt;
&lt;td&gt;Long-running agents that learn over time&lt;/td&gt;
&lt;td&gt;Requires memory ycle management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knowledge graph + vector hybrid&lt;/td&gt;
&lt;td&gt;Knowledge graph + vector database&lt;/td&gt;
&lt;td&gt;Agents that reason over relationships and entities&lt;/td&gt;
&lt;td&gt;Higher implementation complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise context layer&lt;/td&gt;
&lt;td&gt;Governed enterprise data, vector database, knowledge systems&lt;/td&gt;
&lt;td&gt;Regulated industries and enterprise AI&lt;/td&gt;
&lt;td&gt;Higher operational and governance overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Table 1. Comparison of the five AI agent memory architecture patterns.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Pattern 1: In-process (working-only) memory&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The simplest architecture stores everything inside the model's context window. Your agent receives the current conversation, reasons over it, generates a response, and forgets everything once the session ends.&lt;/p&gt;

&lt;p&gt;Working-only memory excels for chatbots, document summarization, content generation, and other short-lived tasks where every request is independent. It is also the easiest architecture to implement because it requires no external storage or retrieval pipeline.&lt;/p&gt;

&lt;p&gt;The tradeoff is that the agent remains stateless. It cannot remember previous conversations, personalize responses over time, or improve through experience. As soon as the context window resets, all memory disappears.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Pattern 2: Flat external vector store&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The next step is to connect your agent to a single vector database. Instead of relying entirely on the context window, the application converts important interactions into embeddings and stores them for future retrieval. When a new request arrives, the application performs vector similarity search and injects the most relevant memories into the prompt.&lt;/p&gt;

&lt;p&gt;This pattern is often the first production-ready architecture teams adopt because it adds cross-session continuity without significantly increasing system complexity. Customer support agents, developer assistants, internal copilots, and knowledge assistants commonly start here.&lt;/p&gt;

&lt;p&gt;The main limitation is scale. As the number of stored memories grows, retrieval quality can decline if every interaction is treated equally. Without a clear write strategy or memory organization, irrelevant memories begin competing with useful ones during retrieval.&lt;/p&gt;

&lt;p&gt;If your data must remain on-premises, VectorAI DB provides the vector storage layer while enabling fast semantic retrieval within your own infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Pattern 3: Tiered memory (episodic and semantic)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Tiered memory separates specific experiences from generalized knowledge. Instead of storing every interaction together, the architecture maintains dedicated memory layers for episodic memory and semantic memory.&lt;/p&gt;

&lt;p&gt;For example, an agent may store a specific customer interaction as episodic memory while gradually learning that the customer prefers concise technical explanations. The individual interaction remains available for detailed recall, while the generalized preference becomes part of semantic memory that influences future responses.&lt;/p&gt;

&lt;p&gt;Tiered memory improves retrieval accuracy because each memory type serves a distinct purpose. It also supports agents that continuously learn without overwhelming the retrieval system with thousands of isolated events.&lt;/p&gt;

&lt;p&gt;The tradeoff is additional complexity. You must define when episodic memories should remain unchanged, when they should be consolidated into semantic knowledge, and how conflicts between the two memory layers should be resolved.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Pattern 4: Knowledge graph and vector hybrid&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Some agents need more than semantic similarity. They must understand relationships between people, systems, projects, documents, and events. A knowledge graph complements vector search by storing explicit relationships that embeddings alone cannot represent.&lt;/p&gt;

&lt;p&gt;For example, a healthcare assistant may need to reason about relationships between patients, medications, diagnoses, and treatment plans. These structured relationships are difficult to reconstruct using vector search alone.&lt;/p&gt;

&lt;p&gt;In this architecture, the knowledge graph answers structured relationship queries, while the vector database retrieves semantically similar documents or experiences. Combining both retrieval methods gives the agent richer context for complex reasoning tasks.&lt;/p&gt;

&lt;p&gt;The tradeoff is implementation complexity. Maintaining two retrieval systems requires more infrastructure, synchronization, and governance than a vector-only architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Pattern 5: Enterprise context layer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Large organizations often cannot store sensitive information in public cloud services or unmanaged memory systems. Instead, AI agents retrieve information from an enterprise context layer that combines governed data sources, access policies, enterprise search, vector databases, and existing knowledge systems.&lt;/p&gt;

&lt;p&gt;This architecture allows the agent to retrieve only the information a user is authorized to access while respecting retention policies, regulatory requirements, and data residency rules. It is common in industries such as &lt;a href="https://www.actian.com/blog/developer/how-to-build-a-hipaa-compliant-ai-ecosystem-without-the-cloud/" rel="noopener noreferrer"&gt;healthcare, where your AI systems have to be HIPAA compliant&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Building a governed memory layer requires identity management, metadata, security policies, auditing, and integration with existing enterprise systems. However, it enables production AI agents that meet organizational compliance requirements while maintaining fast, reliable memory retrieval.&lt;/p&gt;

&lt;p&gt;VectorAI DB fits naturally into Patterns 2, 3, and 5 by providing the vector retrieval layer for organizations that require &lt;a href="https://www.actian.com/blog/databases/when-to-choose-on-premises-vs-cloud-for-vector-databases/" rel="noopener noreferrer"&gt;on-premises, air-gapped, or edge deployments&lt;/a&gt;. In these environments, the vector database acts as the AI memory layer while existing enterprise systems continue to own the underlying business data.&lt;/p&gt;

&lt;p&gt;Choosing the right architecture depends on your application's requirements. Many successful production agents begin with a flat external vector store and gradually evolve into tiered or enterprise architectures as memory volume, governance requirements, and reasoning complexity increase.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Write-Manage-Read Loop&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;An effective AI agent memory architecture is not just about where you store data. It is a continuous write, manage, and read loop that determines what your agent remembers, how that memory evolves, and what information it retrieves during future interactions. If any stage of this loop breaks down, your agent either forgets important information or retrieves memories that are inaccurate, outdated, or irrelevant.&lt;/p&gt;

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

&lt;p&gt;Figure 2. The write, manage, and read lifecycle for AI agent memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Write decides what deserves long-term memory&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Not every interaction belongs in long-term memory. If your agent stores every prompt and response, the memory layer quickly fills with duplicate, low-value information that reduces retrieval quality.&lt;/p&gt;

&lt;p&gt;Instead, define clear write triggers that identify information worth preserving. Strong write signals include confirmed task completion, accepted recommendations, explicit user corrections, and verified outcomes.&lt;/p&gt;

&lt;p&gt;For example, a troubleshooting agent should write a memory only after a fix successfully resolves an incident rather than after every attempted solution. Similarly, a planning agent should store an accepted plan instead of every draft it generates. This approach creates a higher-quality memory base that becomes more valuable over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Manage keeps memory useful over time&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Memory grows continuously, but not every memory should remain equally important forever. Without a management strategy, your memory layer becomes larger, slower, and increasingly difficult to search effectively.&lt;/p&gt;

&lt;p&gt;Memory management includes consolidation, versioning, and retention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consolidation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consolidation transforms repeated episodic memories into semantic knowledge. When dozens of interaction records show the same user preference, the system replaces them with a single semantic memory stating that preference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versioning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Versioning preserves historical records even after new knowledge replaces old information. This allows the agent to understand how knowledge has changed instead of permanently deleting earlier memories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Retention policies remove information that is no longer valuable or that should not persist because of business or regulatory requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Read retrieves the right memory at the right time&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The final stage determines whether the agent can actually use what it has learned. During each request, the application retrieves relevant memories and injects them into the model's context window before inference begins.&lt;/p&gt;

&lt;p&gt;Different memory types require different retrieval methods. &lt;a href="https://atlan.com/know/episodic-memory-ai-agents/#:~:text=Retrieval%20methods%20vary%20by%20framework.%20Vector%20similarity%20search%20is%20most%20common%3B%20temporal%20range%20queries%2C%20BM25%20keyword%20search%2C%20and%20graph%20traversal%20are%20used%20by%20more%20sophisticated%20implementations." rel="noopener noreferrer"&gt;Episodic memories are commonly retrieved using vector similarity search&lt;/a&gt;. Structured business facts may come from a knowledge graph or relational database. Some production systems combine semantic search, keyword search, metadata filtering, and graph queries to maximize retrieval accuracy.&lt;/p&gt;

&lt;p&gt;For agents that rely on vector search, fast approximate nearest neighbor indexing such as &lt;a href="https://endee.io/blog/what-is-hnsw-and-why-is-it-so-fast#:~:text=The%20HNSW%20algorithm%20reduces%20that%2010%2Dmillion%2Dvector%20search%20from%20seconds%20to%20single%2Ddigit%20milliseconds." rel="noopener noreferrer"&gt;HNSW enables low-latency retrieval even as memory grows&lt;/a&gt;. Solutions such as VectorAI DB provide this retrieval layer for organizations that need high-performance semantic search while keeping memory on-premises.&lt;/p&gt;

&lt;p&gt;The quality of your AI agent memory architecture depends on all three stages working together. Writing determines what enters memory, management determines what remains valuable, and retrieval determines whether the right knowledge reaches the model at the right moment. Optimizing retrieval alone cannot compensate for poor write decisions, and storing more information cannot fix a weak memory management strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Common Failure Modes and How to Avoid Them&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Most AI agent memory failures follow a predictable pattern. They occur when teams design the memory architecture without a clear strategy for storing, managing, and retrieving information. Understanding these failure modes before you build your agent is far less expensive than diagnosing them in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Context-resident failure&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A common mistake is treating the context window as long-term memory. While a large context window allows your agent to process more information during a session, everything disappears when that session ends.&lt;/p&gt;

&lt;p&gt;Avoid this by storing important information in long-term memory instead of relying on the context window alone. Working memory should support the current interaction, while episodic and semantic memory should preserve information across sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Retrieval failure&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Some agents have persistent memory but still retrieve irrelevant information. This usually happens because the system stores too much low-value data or uses embeddings that do not represent the application's domain effectively.&lt;/p&gt;

&lt;p&gt;The solution is to define clear write triggers, remove duplicate memories, and regularly evaluate retrieval quality. A smaller collection of high-quality memories almost always performs better than a large collection of noisy data.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Knowledge integrity failure&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;As memory grows, contradictions begin to appear. An episodic memory may state that a user prefers detailed explanations, while a newer semantic memory says the same user now prefers concise responses. Without a strategy for resolving these conflicts, the agent may retrieve inconsistent information and produce unreliable answers.&lt;/p&gt;

&lt;p&gt;Maintain version history for important memories and consolidate repeated experiences into updated semantic knowledge. Keeping historical records while promoting the latest verified knowledge allows your agent to evolve without losing context.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Governance failure&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Persistent memory can also create compliance and security risks. If your agent stores personally identifiable information (PII), confidential business data, or regulated records without appropriate controls, your memory layer quickly becomes a liability.&lt;/p&gt;

&lt;p&gt;This is particularly important in healthcare, finance, government, and other regulated industries where retention policies, access control, and auditability are mandatory.&lt;/p&gt;

&lt;p&gt;To avoid governance failures, define what information your agent is allowed to store, how long it should retain that information, and who can access it. Encrypt sensitive data, implement retention policies, and ensure retrieval respects user permissions and organizational governance rules. These controls become essential as your AI agent memory architecture moves from experimentation to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where to Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The best place to start is with episodic memory backed by a flat external vector store. It gives your agent cross-session continuity without adding unnecessary architectural complexity, and it provides the foundation you can build on as your application grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 1: Define your write triggers&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Before choosing a storage system, decide what information deserves long-term memory. Your agent should not preserve every interaction.&lt;/p&gt;

&lt;p&gt;Focus on high-value events such as confirmed fixes, accepted recommendations, explicit user corrections, completed tasks, and long-term user preferences. Writing only meaningful information keeps your memory layer clean and improves retrieval quality.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 2: Choose a storage pattern&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For most teams, Pattern 2, a flat external vector store, is the best starting point. It is simple to implement, supports semantic similarity search, and gives your agent persistent memory across sessions.&lt;/p&gt;

&lt;p&gt;As your application evolves, you can introduce tiered memory, knowledge graphs, or enterprise context layers without redesigning the entire architecture. Start with the simplest pattern that meets your current requirements, then expand only when new use cases demand additional capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 3: Instrument retrieval before optimizing it&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Many teams focus on improving embeddings or changing vector databases before they understand what the agent actually retrieves.&lt;/p&gt;

&lt;p&gt;Instead, log every retrieval request and inspect the memories returned during inference. Measure how often the retrieved memories contribute to correct responses and identify cases where irrelevant memories appear. These insights help you improve your write strategy, memory organization, and retrieval configuration with real evidence instead of guesswork.&lt;/p&gt;

&lt;p&gt;A memory architecture becomes more effective through observation and refinement. Monitor what your agent writes, manages, and retrieves, then adjust each stage of the memory lifecycle as your application evolves.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Try VectorAI DB Community Edition&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Build your first production-ready memory layer with &lt;a href="https://www.actian.com/databases/vectorai-db/" rel="noopener noreferrer"&gt;VectorAI DB Community Edition&lt;/a&gt;.  Check the &lt;a href="https://docs.vectoraidb.actian.com/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; and participate in the &lt;a href="https://discord.com/invite/432A2M63Py" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; community for support and discussions.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>vectordatabase</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Should You Use RAG or Fine-Tune Your LLM?</title>
      <dc:creator>Offisong Emmanuel</dc:creator>
      <pubDate>Tue, 19 May 2026 19:43:02 +0000</pubDate>
      <link>https://dev.to/actiandev/should-you-use-rag-or-fine-tune-your-llm-3g8j</link>
      <guid>https://dev.to/actiandev/should-you-use-rag-or-fine-tune-your-llm-3g8j</guid>
      <description>&lt;p&gt;The debate over retrieval augmented generation (RAG) vs. fine-tuning appears simple at first glance. RAG pulls in external data at inference time. Fine-tuning modifies model weights during training. In production systems, that distinction is insufficient.&lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://menlovc.com/2024-the-state-of-generative-ai-in-the-enterprise/" rel="noopener noreferrer"&gt;Menlo Ventures 2024&lt;/a&gt; State of Generative AI in the Enterprise report, 51 percent of enterprise AI deployments use RAG in production. Only nine percent rely primarily on fine-tuning. Yet research such as the &lt;a href="https://shishirpatil.github.io/publications/raft-2024.pdf" rel="noopener noreferrer"&gt;RAFT study&lt;/a&gt; from UC Berkeley shows that hybrid systems combining retrieval and fine-tuning outperform either approach alone across benchmarks.&lt;/p&gt;

&lt;p&gt;If hybrid systems can produce better results, why does industry adoption favor only RAG? In this article, we’ll compare RAG, fine-tuning, and a hybrid architecture to understand the trade-offs and where each approach excels.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG&lt;/strong&gt;: Best for frequently changing knowledge and moderate traffic; easy to update without retraining.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-&lt;/strong&gt;&lt;strong&gt;t&lt;/strong&gt;&lt;strong&gt;uning&lt;/strong&gt;: Best for stable domains and high-volume or low-latency tasks; improves task-specific accuracy and formatting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid/RAFT&lt;/strong&gt;: Combines up-to-date retrieval with optimized model behavior for the highest accuracy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key&lt;/strong&gt; &lt;strong&gt;t&lt;/strong&gt;&lt;strong&gt;rade-off&lt;/strong&gt;: Choice depends on query volume, how often knowledge changes, and team expertise. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the Standard RAG vs. Fine-Tuning Comparison Fails
&lt;/h2&gt;

&lt;p&gt;RAG is a method where the model dynamically pulls in external data at inference time. Each query retrieves relevant documents or knowledge chunks, which the system appends to the prompt, allowing the model to produce answers grounded in current information.&lt;/p&gt;

&lt;p&gt;Fine-tuning is the process of modifying a model’s weights during training using labeled data. Instead of relying on external retrieval, the model internalizes patterns directly, producing consistent outputs without querying external sources.&lt;/p&gt;

&lt;p&gt;While these definitions are technically correct, most standard comparisons miss the factors that actually drive decisions in production. In real-world systems, the choice between RAG and fine-tuning depends on variables like scale, query volume, and how often your data changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing variable 1: Context expansion at scale
&lt;/h3&gt;

&lt;p&gt;In many production RAG systems, every request appends hundreds of tokens. That added context changes how the model allocates attention and prioritizes weights.&lt;/p&gt;

&lt;p&gt;Large retrieved contexts compete for attention with the prompt and instructions, which can dilute signal quality. Small retrieval errors or loosely relevant chunks can introduce formatting drift, or shift reasoning in subtle ways. The system’s output becomes tightly coupled to retrieval quality.&lt;/p&gt;

&lt;p&gt;Fine-tuning works differently. Instead of injecting large volumes of text at inference time, it embeds patterns and constraints directly into the model during training. The distinction affects how the system behaves under real workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing variable 2: Retraining frequency
&lt;/h3&gt;

&lt;p&gt;The common advice says “use RAG if knowledge changes frequently" and “use fine-tuning if behavior is stable.” But how frequently is “frequently”?&lt;/p&gt;

&lt;p&gt;If your knowledge base changes daily, retraining pipelines may introduce operational friction. Evaluation cycles, dataset versioning, and deployment validation all add delay.&lt;/p&gt;

&lt;p&gt;Data preparation also matters. If your organization lacks structured, versioned, and clean datasets, the hidden cost of &lt;a href="https://www.actian.com/blog/generative-ai/data-preparation-guide-generative-ai-adoption/" rel="noopener noreferrer"&gt;preparing training data&lt;/a&gt; can exceed compute costs. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Math of RAG vs. Fine-Tuning
&lt;/h2&gt;

&lt;p&gt;Surface-level comparisons of RAG and fine-tuning often ignore the cost curves that determine long-term viability. In production systems, financial estimations are crucial in architectural decisions. To evaluate RAG vs. fine-tuning realistically, we need to examine three cost layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Token cost and context expansion&lt;/li&gt;
&lt;li&gt;Retrieval infrastructure cost&lt;/li&gt;
&lt;li&gt;Training infrastructure cost&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The cost structure of RAG
&lt;/h3&gt;

&lt;p&gt;RAG systems introduce a recurring operational cost because each query retrieves external information and injects it into the model’s prompt. That additional context is billed on every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context expansion&lt;/strong&gt;&lt;br&gt;
Production RAG systems append around 500 tokens of retrieved context to each query. The provider bills those tokens on every request.&lt;/p&gt;

&lt;p&gt;Using pricing similar to &lt;a href="https://openai.com/api/pricing/" rel="noopener noreferrer"&gt;GPT-5.2 at 1.750 dollars&lt;/a&gt; per million input tokens, the incremental monthly cost becomes:&lt;/p&gt;

&lt;p&gt;Cost per query &lt;br&gt;
500 tokens × $1.75/1,000,000 = $0.000875 per query&lt;/p&gt;

&lt;p&gt;At a small scale, this cost appears negligible. However, because it applies to every query, the total overhead grows linearly with traffic.&lt;/p&gt;

&lt;p&gt;At different traffic levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Monthly queries&lt;/th&gt;
&lt;th&gt;Context cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10 million&lt;/td&gt;
&lt;td&gt;$8,750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 million&lt;/td&gt;
&lt;td&gt;$43,750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 million&lt;/td&gt;
&lt;td&gt;$87,500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is context overhead alone. It does not include output tokens or base prompt tokens. At a sustained scale, what appears flexible and inexpensive becomes a significant recurring expense. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector database and retrieval cost&lt;/strong&gt;&lt;br&gt;
Token cost is only one component of RAG costs. RAG also relies on a vector database for semantic search. The system must store, index, and query embeddings efficiently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pinecone.io/pricing/estimate/" rel="noopener noreferrer"&gt;Public pricing&lt;/a&gt; of Pinecone lists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storage at approximately 0.33 dollars per gigabyte per month&lt;/li&gt;
&lt;li&gt;Read units at approximately 16 dollars per million&lt;/li&gt;
&lt;li&gt;Write units at approximately four dollars per million&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, consider a system handling 50 million queries per month, where each query performs a single vector search (assuming a 1,024-dimension vector). That would result in 50 million read operations monthly. If the system also writes approximately six million records per month, the combined read and write activity would bring the total estimated monthly cost to around $1,532.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjb79lejvljq0xezfw1y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjb79lejvljq0xezfw1y.png" alt="Figure 1: Pinecone pricing for 50M vectors" width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At 200 million queries per month, the total expenses rises to $9,000 per month.&lt;/p&gt;

&lt;p&gt;Two RAG systems serving identical traffic can therefore have materially different cost structures depending on &lt;a href="https://dev.to/actiandev/whats-changing-in-vector-databases-in-2026-3pbo"&gt;how the vector database is designed&lt;/a&gt; and optimized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure cost&lt;/strong&gt;&lt;br&gt;
RAG systems require storage and compute infrastructure to generate embeddings, store and index vectors, execute retrieval queries, and run inference. Each of these stages consumes compute resources, typically provisioned through cloud servers that must scale with traffic.&lt;/p&gt;

&lt;p&gt;For real-time or high-throughput applications, additional capacity is required to maintain low latency and system reliability. Replication, autoscaling, monitoring, and failover mechanisms all add operational complexity. These infrastructure layers are essential for production-grade RAG, but they expand the total cost footprint beyond token usage alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost structure of fine-tuning
&lt;/h3&gt;

&lt;p&gt;Fine-tuning introduces a different economic model from RAG systems. Instead of paying incremental costs on every request for external context, you invest upfront to modify the model’s internal behavior.&lt;/p&gt;

&lt;p&gt;That upfront investment can be broken into four primary cost categories: data, training compute, experimentation, and operational maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data preparation costs&lt;/strong&gt;&lt;br&gt;
High-quality labeled data is the foundation of effective fine-tuning. This includes collecting domain-specific examples, cleaning inconsistencies, formatting inputs and outputs correctly, and validating annotation quality.&lt;/p&gt;

&lt;p&gt;In many organizations, &lt;a href="https://www.centage.com/blog/how-to-calculate-the-roi-of-ai-a-guide-for-finance-leaders-2025-edition" rel="noopener noreferrer"&gt;data preparation consumes 20 to 40 percent&lt;/a&gt; of the total fine-tuning budget. Poorly curated data directly degrades model performance, leading to additional retraining cycles and wasted compute. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training compute costs&lt;/strong&gt;&lt;br&gt;
OpenAI lists fine-tuning at roughly $25 per million training tokens for GPT-4.1. A run using 20 million tokens would cost about $500 in direct training fees, with larger datasets or multiple runs increasing this total.&lt;/p&gt;

&lt;p&gt;For self-hosted training, costs depend on model size and hardware. High-performance GPUs such as A100 clusters can cost thousands of dollars per training epoch. Because fine-tuning is rarely a single-pass process, multiple epochs, evaluations, and retraining cycles are common, which further increases the overall cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experimentation and validation costs&lt;/strong&gt;&lt;br&gt;
Fine-tuning is an iterative process that requires experimentation with hyperparameters, evaluation against baseline models, and testing across edge cases. These workflows require engineering time, infrastructure, and structured evaluation frameworks. Unlike prompt engineering, fine-tuning introduces a full ML lifecycle, adding ongoing operational overhead.&lt;/p&gt;

&lt;p&gt;This creates a non-linear cost curve. Fine-tuning concentrates cost at the beginning, while marginal cost per request remains relatively stable as traffic grows.&lt;/p&gt;

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

&lt;p&gt;Whether that trade-off is advantageous depends on three variables: query volume, knowledge stability, and retraining frequency. Without modeling those explicitly, cost comparisons between RAG and fine-tuning remain incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  When RAG Wins
&lt;/h2&gt;

&lt;p&gt;Despite its scaling trade-offs, RAG remains the dominant production choice for a reason. In certain operating conditions, it is structurally more flexible, faster to iterate, and operationally safer than fine-tuning. RAG is suitable in the following scenarios:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. When knowledge changes frequently
&lt;/h3&gt;

&lt;p&gt;If your domain knowledge changes weekly or daily, fine-tuning becomes operationally expensive. Dataset updates, retraining, evaluation, and deployment introduce delays that can stretch from hours to weeks depending on governance requirements.&lt;/p&gt;

&lt;p&gt;Teams frequently underestimate the operational overhead of keeping a fine-tuned model synchronized with a rapidly evolving knowledge base. In these environments, RAG shifts the problem from model retraining to data indexing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. When you have extensive unstructured data but limited labeled data
&lt;/h3&gt;

&lt;p&gt;Many organizations possess terabytes of internal documents but lack high-quality supervised datasets. Building labeled training corpora requires annotation workflows, domain experts, and quality validation pipelines. In practice, this often becomes the most expensive part of fine-tuning projects.&lt;/p&gt;

&lt;p&gt;RAG bypasses this constraint by allowing models to operate directly on existing document corpora without constructing large labeled datasets.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. When governance and data residency requirements are strict
&lt;/h3&gt;

&lt;p&gt;Once sensitive information is embedded in model weights, deletion and auditing become difficult. Removing a specific record from a fine-tuned model often requires retraining or maintaining complex dataset lineage.&lt;/p&gt;

&lt;p&gt;RAG architectures avoid this issue by keeping sensitive information in external storage systems where standard governance controls already exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. When query volume is moderate
&lt;/h3&gt;

&lt;p&gt;As shown in the earlier cost analysis, context expansion overhead grows with query volume, reaching approximately $43,750 per month at 50 million queries. At moderate traffic, RAG’s per-request costs are typically lower than the amortized expenses of fine-tuning, including training and ongoing maintenance. This makes RAG an attractive choice for organizations that want high-quality outputs without front-loading infrastructure and compute investments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use cases
&lt;/h3&gt;

&lt;p&gt;Large-scale examples illustrate RAG’s effectiveness at this volume. Notion’s Q&amp;amp;A assistant is &lt;a href="https://www.supervised.news/p/how-notion-is-tackling-one-of-ais" rel="noopener noreferrer"&gt;effectively a large-scale RAG system&lt;/a&gt; over workspace data. The difficult engineering problem was not retrieval itself, but enforcing identity and access controls during retrieval. When a user queries the assistant, the system must ensure the model only retrieves documents that the user is permitted to see. &lt;/p&gt;

&lt;p&gt;LinkedIn leveraged RAG and knowledge graphs to preserve the structure of their support cases. This system retrieved relevant subgraphs rather than isolated text chunks, improving retrieval accuracy by 77.6% and &lt;a href="https://arxiv.org/html/2404.17723v1" rel="noopener noreferrer"&gt;reducing median issue resolution time by 28.6%&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For systems at this scale, RAG combines cost efficiency with flexibility, allowing teams to update knowledge sources rapidly without retraining models, while still delivering high-quality results.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Fine-Tuning Wins
&lt;/h2&gt;

&lt;p&gt;Fine-tuning becomes structurally advantageous under different conditions. These conditions typically involve scale, stability, and behavioral precision.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. When query volume exceeds 100 million per month
&lt;/h3&gt;

&lt;p&gt;At very high traffic levels (100M+ queries per month), RAG’s per-request context overhead becomes significant. Each query adds hundreds of retrieved tokens that the model processes, causing costs to scale linearly with traffic. Large context windows can also increase latency, reduce throughput, and complicate infrastructure reliability.&lt;/p&gt;

&lt;p&gt;If domain knowledge is relatively stable, fine-tuning can become more efficient. By embedding knowledge directly into the model, organizations avoid repeated retrieval and token costs, leading to more predictable per-query expenses, better consistency, and simpler operations at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. When output structure is critical
&lt;/h3&gt;

&lt;p&gt;Fine-tuned models often excel in tasks that require strict adherence to structure or formal constraints. For example, &lt;a href="https://cosine.sh/" rel="noopener noreferrer"&gt;Cosine&lt;/a&gt;, which is an AI software engineering assistant that’s able to autonomously resolve bugs and build features, was able to achieve a &lt;a href="https://openai.com/index/gpt-4o-fine-tuning/" rel="noopener noreferrer"&gt;SOTA score of 43.8%&lt;/a&gt; on the SWE-bench⁠ verified benchmark. &lt;/p&gt;

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

&lt;p&gt;Similarly, &lt;a href="https://distyl.ai/" rel="noopener noreferrer"&gt;Distyl&lt;/a&gt; secured the top position on the BIRD-SQL benchmark, widely regarded as the premier evaluation for text-to-SQL performance. Its fine-tuned GPT-4o model reached an execution accuracy of 71.83% on the leaderboard.&lt;/p&gt;

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

&lt;p&gt;In applications where errors propagate downstream, into financial calculations, automated APIs, or compliance documents, behavioral consistency is mandatory. In these contexts, fine-tuning provides the reliability needed to minimize risk and maintain trust in automated outputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. When latency requirements are strict
&lt;/h3&gt;

&lt;p&gt;RAG adds multiple steps to the inference pipeline that increase response time. Each query must go through embedding generation, vector search, and context injection before reaching the model.&lt;/p&gt;

&lt;p&gt;Fine-tuned models skip retrieval entirely. All necessary knowledge and reasoning patterns are internalized, allowing the model to generate outputs immediately. In applications where sub-100ms responses are required, such as live recommendation engines or high-frequency trading systems, removing the retrieval pipeline eliminates a major bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. When deep domain reasoning matters more than freshness
&lt;/h3&gt;

&lt;p&gt;A domain-specific agriculture benchmark study found that &lt;a href="https://cension.ai/blog/ai-rag-fine-tuning-cheaper-hallucinations/" rel="noopener noreferrer"&gt;fine-tuning improved model accuracy from 75% to 81%&lt;/a&gt;, while hybrid systems (fine-tuning + retrieval) reached 86%. Because the dataset focused on specialized agricultural knowledge and reasoning tasks, the improvement primarily reflects stronger domain reasoning, not simply better access to external information.&lt;/p&gt;

&lt;p&gt;In domains such as legal analysis or medical decision support, reasoning patterns can be complex. Fine-tuning enables models to internalize domain expertise rather than rely solely on retrieved context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Approach
&lt;/h2&gt;

&lt;p&gt;While RAG and fine-tuning each have clear advantages, research shows that combining them effectively can produce superior results, but only when done correctly. The RAFT (Retrieval Augmented Fine-Tuning) approach, developed by UC Berkeley, Microsoft, and Meta Research, &lt;a href="https://arxiv.org/pdf/2403.10131" rel="noopener noreferrer"&gt;demonstrates how to do this&lt;/a&gt; in practice.&lt;/p&gt;

&lt;p&gt;RAFT trains a model to operate in an “open-book” setting. It learns to process retrieved context, identify relevant passages, ignore distractors, and cite evidence accurately. Without this explicit training, simply layering RAG on top of a fine-tuned model often fails. For instance, a model fine-tuned on medical reasoning may retrieve irrelevant journal articles if it hasn’t learned to filter and prioritize context, resulting in hallucinations or incorrect recommendations.&lt;/p&gt;

&lt;p&gt;RAFT addresses this with a &lt;a href="https://arxiv.org/html/2506.22644v1" rel="noopener noreferrer"&gt;structured 80/20 training split&lt;/a&gt;. 80% of training examples include oracle documents that the model should use, and 20% do not, forcing the model to learn when to trust retrieved data and when to rely on internalized knowledge. This operational detail is crucial for engineers evaluating whether their team can implement a hybrid approach successfully. It is not enough to just combine RAG and fine-tuning. The model must be trained to reason over the retrieved context.&lt;/p&gt;

&lt;p&gt;A common and practical pattern is “fine-tune for format, RAG for knowledge.” Fine-tuning shapes the model’s internal behavior, enforcing domain-specific reasoning, output structure, and style. RAG provides dynamic access to external information that changes frequently or is too large to store in the model weights. In healthcare, for example, fine-tuning ensures the model understands medical terminology, follows proper diagnostic reasoning, and formats outputs according to clinical documentation standards. RAG supplements this by retrieving the latest research, newly published treatment guidelines, or patient-specific records, keeping recommendations current without retraining the entire model.&lt;/p&gt;

&lt;p&gt;Similarly, Harvey AI &lt;a href="https://newsletter.himanshuramchandani.co/p/harvey-ai-5b-legal-fine-tuning-case-study" rel="noopener noreferrer"&gt;fine-tuned on 10 billion case law tokens&lt;/a&gt;, but still leverages RAG to handle current cases and updates. This pattern is widely used in other domains too. Legal systems fine-tune for statutory reasoning and citation style, then layer RAG to retrieve the most current case law; finance models fine-tune for portfolio analysis rules, then layer RAG for market updates and regulatory changes. It’s a way to balance the stability of learned behavior with the adaptability of retrieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quantified Decision Framework for RAG vs. Fine-Tuning
&lt;/h2&gt;

&lt;p&gt;The question is no longer “Which approach is better?” It is “Under what conditions does each approach make economic and operational sense?”&lt;/p&gt;

&lt;p&gt;Instead of defaulting to architectural preference, evaluate three measurable variables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Knowledge change frequency&lt;/li&gt;
&lt;li&gt;Monthly query volume&lt;/li&gt;
&lt;li&gt;Infrastructure capability and governance constraints&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When those variables are quantified, the decision becomes far clearer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Measure knowledge volatility
&lt;/h3&gt;

&lt;p&gt;Knowledge change frequency is often the fastest way to eliminate one option. If your domain knowledge changes weekly or daily, RAG is structurally favored. Updating an index is far simpler than retraining a fine-tuned model. The separation between model weights and external data enables real time data retrieval without redeployment cycles.&lt;/p&gt;

&lt;p&gt;If knowledge remains stable for months at a time, fine-tuning becomes economically viable. Retraining frequency drops, and training cost can be amortized over longer intervals. In these environments, embedding domain specific knowledge directly into model parameters may reduce long-term inference overhead.&lt;/p&gt;

&lt;p&gt;As a practical threshold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Knowledge changes more than monthly → prioritize RAG&lt;/li&gt;
&lt;li&gt;Knowledge stable for multiple months → evaluate fine-tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Calculate context expansion cost**
&lt;/h3&gt;

&lt;p&gt;The next variable is query volume. Large-scale RAG systems append hundreds of tokens to every query, and this context overhead scales linearly with traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quantitative triggers&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Monthly queries&lt;/th&gt;
&lt;th&gt;Guidance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;10M&lt;/td&gt;
&lt;td&gt;RAG is cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10–50M&lt;/td&gt;
&lt;td&gt;Evaluate fine-tuning vs. RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50–100M&lt;/td&gt;
&lt;td&gt;Fine-tuning or hybrid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;100M&lt;/td&gt;
&lt;td&gt;Fine-tuning or hybrid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 3: Assess infrastructure maturity
&lt;/h3&gt;

&lt;p&gt;Even if economics favor one approach, infrastructure capability may dictate feasibility.&lt;/p&gt;

&lt;p&gt;RAG requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong data engineering&lt;/li&gt;
&lt;li&gt;Reliable data pipelines&lt;/li&gt;
&lt;li&gt;Efficient vector database architecture&lt;/li&gt;
&lt;li&gt;Observability and monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fine-tuning requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High quality labeled data&lt;/li&gt;
&lt;li&gt;Machine learning expertise&lt;/li&gt;
&lt;li&gt;Compute resource allocation&lt;/li&gt;
&lt;li&gt;Evaluation discipline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When teams ignore their actual capabilities, architecture decisions collapse under scale. Many production failures blamed on “model quality” are just traits of immature infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;p&gt;The following matrix translates the analysis into practical guidance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Monthly&lt;/strong&gt; &lt;strong&gt;q&lt;/strong&gt;&lt;strong&gt;ueries&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Knowledge&lt;/strong&gt; &lt;strong&gt;u&lt;/strong&gt;&lt;strong&gt;pdate&lt;/strong&gt; &lt;strong&gt;f&lt;/strong&gt;&lt;strong&gt;requency&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Recommendation&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Rationale&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain knowledge updates weekly, moderate traffic&lt;/td&gt;
&lt;td&gt;10–50M&lt;/td&gt;
&lt;td&gt;Weekly/Daily&lt;/td&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;td&gt;Immediate indexing and low recurring cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-scale traffic, knowledge stable&lt;/td&gt;
&lt;td&gt;50–100M+&lt;/td&gt;
&lt;td&gt;&amp;lt;1 update/month&lt;/td&gt;
&lt;td&gt;Fine-tuning&lt;/td&gt;
&lt;td&gt;Avoids recurring context injection, reduces latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output or code generation required&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;Fine-tuning&lt;/td&gt;
&lt;td&gt;Embeds domain-specific rules and formatting internally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specialized reasoning + frequent updates&lt;/td&gt;
&lt;td&gt;10–50M&lt;/td&gt;
&lt;td&gt;Weekly/Daily&lt;/td&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;td&gt;Combines internalized reasoning with dynamic knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-domain systems with diverse knowledge update cycles&lt;/td&gt;
&lt;td&gt;10–100M&lt;/td&gt;
&lt;td&gt;Mixed&lt;/td&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;td&gt;Fine-tuning stabilizes core domains, RAG handles rapidly changing sources&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using this matrix, it becomes easier to make the decision whether to utilize RAG, fine-tune your LLMs, or use the hybrid approach. &lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The debate between RAG and fine-tuning is often framed as a binary choice, but the more useful question is “If hybrid systems demonstrably outperform either approach alone, why does industry adoption still overwhelmingly favor RAG?” &lt;/p&gt;

&lt;p&gt;Hybrid requires both ML and data engineering capabilities simultaneously, a combination few organizations have. RAG remains the practical default, offering agility and transparency with less upfront complexity.&lt;/p&gt;

&lt;p&gt;The key takeaway is to choose the architecture that matches your knowledge volatility, query scale, and team capability. For teams exploring enterprise-scale retrieval systems, platforms like Actian &lt;a href="https://www.actian.com/databases/vectorai-db/#waitlist" rel="noopener noreferrer"&gt;VectorAI DB&lt;/a&gt; provide purpose-built vector database capabilities designed for performance and scalability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://discord.com/invite/432A2M63Py" rel="noopener noreferrer"&gt;&lt;em&gt;Join the Discord community&lt;/em&gt;&lt;/a&gt; &lt;em&gt;and learn how Actian fits to your AI strategy.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>rag</category>
    </item>
    <item>
      <title>What 37signals’ Cloud Repatriation Taught Us About AI Infrastructure</title>
      <dc:creator>Offisong Emmanuel</dc:creator>
      <pubDate>Tue, 19 May 2026 19:42:51 +0000</pubDate>
      <link>https://dev.to/actiandev/what-37signals-cloud-repatriation-taught-us-about-ai-infrastructure-2hp</link>
      <guid>https://dev.to/actiandev/what-37signals-cloud-repatriation-taught-us-about-ai-infrastructure-2hp</guid>
      <description>&lt;p&gt;In 2023, &lt;a href="https://37signals.com/" rel="noopener noreferrer"&gt;37signals&lt;/a&gt; announced that it had completely left the public cloud and followed up by publicly documenting its cloud repatriation process, providing one of the clearest real-world examples of on-premises economics at scale. By reversing its cloud migration and shifting workloads to private cloud infrastructure, the company drastically &lt;a href="https://www.datacenterdynamics.com/en/news/37signals-claims-it-saved-almost-2m-last-year-from-cloud-repatriation/" rel="noopener noreferrer"&gt;reduced its annual cloud infrastructure spend by almost $2 million&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The transparency of the numbers made the case compelling. In 2022, &lt;a href="https://dev.37signals.com/our-cloud-spend-in-2022/" rel="noopener noreferrer"&gt;37signals spent $3,201,564 on cloud services&lt;/a&gt;, which is about $266,797 per month. These detailed cost breakdowns, along with published hardware investment and payback timelines, provided a rare look into the financial mechanics of large-scale cloud repatriation.&lt;/p&gt;

&lt;p&gt;For commodity SaaS workloads, the math was clear. But the same logic raises an important question for the next generation of compute-heavy systems: “Does the economic argument extend to AI infrastructure as well?” In this article, we examine whether the same economic logic holds for AI infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;37signals spent ~$3.2M/year on AWS in 2022.&lt;/li&gt;
&lt;li&gt;After repatriating workloads to their own infrastructure, cloud spend dropped to ~$1.3M by 2024.&lt;/li&gt;
&lt;li&gt;The company invested roughly $700K–$800K in servers and paid them off in under 18 months.&lt;/li&gt;
&lt;li&gt;The entire infrastructure is still run by the same 10-person team. No additional operational overhead.&lt;/li&gt;
&lt;li&gt;The key takeaway is that at a sustained scale, owning infrastructure can be dramatically cheaper than renting it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 37signals Playbook: What Hanson Actually Documented
&lt;/h2&gt;

&lt;p&gt;In 2022, 37signals &lt;a href="https://dev.37signals.com/our-cloud-spend-in-2022/" rel="noopener noreferrer"&gt;spent $3.2 million annually on AWS&lt;/a&gt;. After &lt;a href="https://world.hey.com/dhh/we-have-left-the-cloud-251760fb" rel="noopener noreferrer"&gt;leaving the cloud in 2023&lt;/a&gt;, their annual costs had dropped to approximately $1.3 million by 2024, a &lt;a href="https://www.datacenterdynamics.com/en/news/37signals-claims-it-saved-almost-2m-last-year-from-cloud-repatriation/" rel="noopener noreferrer"&gt;reduction of almost $2 million&lt;/a&gt; per year.&lt;/p&gt;

&lt;p&gt;The transition required a hardware investment of roughly &lt;a href="https://world.hey.com/dhh/the-big-cloud-exit-faq-20274010" rel="noopener noreferrer"&gt;$600,000 in Dell servers&lt;/a&gt;. The company fully recouped the investment in under 18 months, achieving complete payback in the second half of 2023 as their AWS reserved instance contracts expired. From that point forward, the savings flowed directly to operating margin rather than offsetting capital expense.&lt;/p&gt;

&lt;p&gt;37signals projected $1.5 million in hardware costs and roughly $200,000 per year in operating expenses. This shift replaces a recurring $1.3 million annual cloud storage bill with a one-time capital outlay plus a fraction of the ongoing operating cost. Over five years, 37signals revised the total savings projections upward from $7 million to more than $10 million.&lt;/p&gt;

&lt;h3&gt;
  
  
  37signals cloud exit financials by year
&lt;/h3&gt;

&lt;p&gt;To illustrate the financial impact of 37signals’ cloud exit over time, the table below breaks down annual &lt;a href="https://www.actian.com/blog/databases/when-to-choose-on-premises-vs-cloud-for-vector-databases/" rel="noopener noreferrer"&gt;cloud spending, on-premises hardware investments&lt;/a&gt;, and operating costs, highlighting the resulting net savings and key operational notes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Year&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Cloud&lt;/strong&gt; &lt;strong&gt;s&lt;/strong&gt;&lt;strong&gt;pend&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Hardware&lt;/strong&gt; &lt;strong&gt;i&lt;/strong&gt;&lt;strong&gt;nvestment&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Operating&lt;/strong&gt; &lt;strong&gt;c&lt;/strong&gt;&lt;strong&gt;osts&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2022 Baseline&lt;/td&gt;
&lt;td&gt;~$3.2M&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Included in cloud spend&lt;/td&gt;
&lt;td&gt;Full cloud dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023 Migration&lt;/td&gt;
&lt;td&gt;~$2M&lt;/td&gt;
&lt;td&gt;~$700–800K&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Hardware fully recouped in under 18 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024+ Post-repatriation&lt;/td&gt;
&lt;td&gt;~$1.3M&lt;/td&gt;
&lt;td&gt;~$1.5M (storage)&lt;/td&gt;
&lt;td&gt;~$200K/year&lt;/td&gt;
&lt;td&gt;~$1.9M annual savings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025+&lt;/td&gt;
&lt;td&gt;Minimal AWS dependency&lt;/td&gt;
&lt;td&gt;~$1.5M (Pure Storage, 18PB)&lt;/td&gt;
&lt;td&gt;~$200K/year&lt;/td&gt;
&lt;td&gt;$10M+ projected 5-year savings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notably, the migration did not require the team to expand operations. A 10-person infrastructure team handled the entire repatriation without adding new staff. Addressing a common concern about operational overhead, &lt;a href="https://world.hey.com/dhh/our-cloud-exit-savings-will-now-top-ten-million-over-five-years-c7d9b5bd" rel="noopener noreferrer"&gt;37signals co-founder David Heinemeier Hansso&lt;/a&gt;&lt;a href="https://world.hey.com/dhh/our-cloud-exit-savings-will-now-top-ten-million-over-five-years-c7d9b5bd" rel="noopener noreferrer"&gt;n noted:&lt;/a&gt;&lt;br&gt;
&lt;a href="https://world.hey.com/dhh/our-cloud-exit-savings-will-now-top-ten-million-over-five-years-c7d9b5bd" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We've been out for just over a year now, and the team managing everything is still the same. There were no hidden dragons of additional workload associated with the exit that required us to balloon the team, as some spectators speculated when we announced it. All the answers in our &lt;a href="https://world.hey.com/dhh/the-big-cloud-exit-faq-20274010" rel="noopener noreferrer"&gt;Big Cloud Exit FAQ&lt;/a&gt; continue to hold.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This directly challenges the common assumption that moving away from public cloud environments inevitably requires a significantly larger infrastructure team.&lt;/p&gt;

&lt;p&gt;Execution followed a “criticality ladder” strategy where the team migrated lower-risk services first and more critical ones later. The team moved the HEY email system in stages, starting with caching, then database, and finally, job services. To minimize risk, they colocated infrastructure approximately one millisecond from the AWS region to preserve rollback capability during the cloud repatriation process. After stabilizing the system, they replaced managed services with substantial recurring cost, including RDS and managed Elasticsearch which exceeded $500,000 together annually.&lt;/p&gt;

&lt;p&gt;What makes 37signals' case study consequential is the publicly documented cost efficiency. For organizations questioning long-term &lt;a href="https://www.actian.com/blog/cloud-data-warehouse/will-cloud-data-warehouses-really-help-you-cut-costs/" rel="noopener noreferrer"&gt;cloud adoption assumptions&lt;/a&gt; particularly with regard to storage costs and managed services, the 37signals documentation provides a rare baseline for comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI Infrastructure Economics Are Even More Extreme
&lt;/h2&gt;

&lt;p&gt;The lessons from 37signals’ cloud repatriation take on a sharper edge when applied to AI infrastructure. Higher GPU costs, predictable inference workloads, massive embedding storage, and stricter data regulations create financial and operational pressures that amplify the advantages of &lt;a href="https://www.actian.com/on-premises-data/" rel="noopener noreferrer"&gt;on-premises&lt;/a&gt; or hybrid cloud solutions that allow you to move workloads where they make the most sense. Below, we break down the key drivers.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI infrastructure cost comparison
&lt;/h3&gt;

&lt;p&gt;To evaluate the cost implications of different AI infrastructure approaches, the table below compares upfront setup costs, monthly operating expenses at varying workloads, and expected break-even timelines for cloud, on-premises, and hybrid configurations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Setup&lt;/strong&gt; &lt;strong&gt;c&lt;/strong&gt;&lt;strong&gt;ost&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Monthly&lt;/strong&gt; &lt;strong&gt;c&lt;/strong&gt;&lt;strong&gt;ost&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Break-even&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud GPU rental (AWS/ Azure)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$2,900–3,500 (8h/day × $4–8/hour × 15 days)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud inference APIs (Lambda Labs)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$1,800–2,500 (8h/day × $3.67/hour × 15 days)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted GPU (8×H100 server)&lt;/td&gt;
&lt;td&gt;$200K–400K&lt;/td&gt;
&lt;td&gt;$1,500–2,000 (power + maintenance)&lt;/td&gt;
&lt;td&gt;&amp;lt;12 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid (Cloud training + On-Prem)&lt;/td&gt;
&lt;td&gt;$200K–400K&lt;/td&gt;
&lt;td&gt;Training only, inference minimal&lt;/td&gt;
&lt;td&gt;&amp;lt;12 months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: For cloud GPU rental, we estimate monthly cost assuming eight hours/day per GPU. The cost scales linearly with utilization; it is not directly per-query.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1. GPU cloud markups are high
&lt;/h3&gt;

&lt;p&gt;AI workloads depend heavily on GPUs, and cloud providers charge far steeper premiums for GPU capacity than for typical CPU compute. On-demand AWS P5 instances with H100 GPUs cost roughly $4–8 per GPU-hour, while comparable Azure H100 instances are about $3.67 per hour. By contrast, spot markets and alternative providers such as &lt;a href="https://lambda.ai/" rel="noopener noreferrer"&gt;Lambda Labs&lt;/a&gt; offer similar GPU capacity for $1–2 per hour, or $1.85–2.49 per hour with reserved commitments. &lt;/p&gt;

&lt;p&gt;The result is a 4–8× markup for on-demand hyperscaler GPU capacity relative to the spot or specialized GPU cloud market. In other words, the premium cloud providers charge for high-end AI compute is significantly larger than typical CPU cloud markups. For organizations running sustained inference workloads, this pricing gap quickly becomes the dominant cost driver in AI infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Predictable inference makes GPU ownership economical
&lt;/h3&gt;

&lt;p&gt;High GPU pricing becomes especially significant because AI inference workloads are unusually predictable. Purchasing H100 GPUs outright can be cost-efficient. A single GPU costs roughly $25K–40K, while a complete 8×H100 server ranges from $200K–400K. &lt;a href="https://lenovopress.lenovo.com/lp2225-on-premise-vs-cloud-generative-ai-total-cost-of-ownership-2025-edition" rel="noopener noreferrer"&gt;Lenovo’s analysis&lt;/a&gt; shows that six or more hours of sustained daily usage reaches payback against AWS within the first year.&lt;/p&gt;

&lt;p&gt;The reason this break-even arrives so quickly is that AI inference workloads are unusually predictable. Unlike SaaS traffic which fluctuates throughout the day, production AI systems such as recommendation engines tend to process steady volumes of requests.&lt;/p&gt;

&lt;p&gt;Predictability changes the economics. When infrastructure runs at consistent utilization, owned hardware can be amortized efficiently across the workload. Paying cloud premiums for burst capacity that teams rarely use becomes unnecessary.&lt;/p&gt;

&lt;p&gt;For organizations running inference continuously, the hardware investment is often recouped in under 12 months. From that point forward, the savings resemble the same pattern documented by 37signals. Fixed infrastructure replacing an ongoing rental bill.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Embedding storage requirements are massive
&lt;/h3&gt;

&lt;p&gt;Even if GPU compute were optimized, AI systems introduce another rapidly growing cost layer: embedding storage. &lt;a href="https://dev.to/actiandev/whats-changing-in-vector-databases-in-2026-3pbo"&gt;Vector databases&lt;/a&gt; store high-dimensional embeddings used for search, retrieval, and recommendation. As datasets scale into millions or billions of records, storage requirements expand quickly.&lt;/p&gt;

&lt;p&gt;For instance, 10 million vectors at 1,536 dimensions require at least 58GB of raw storage, often 200–300GB with indexes and metadata. Cloud storage services like Pinecone charge $0.33/GB/month, meaning 500GB could cost $165/month before any queries. Self-hosted solutions like PostgreSQL with pgvector dramatically reduce cloud spending while keeping sensitive data under direct control. Over time, these storage requirements compound infrastructure costs alongside GPU compute, further reinforcing the economic advantages of self-hosted or hybrid architectures.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Data sovereignty and compliance favor on-premises deployment
&lt;/h3&gt;

&lt;p&gt;Data residency regulations and general compliance are priorities in the AI space with the industry becoming increasingly regulated. Notably, the EU AI Act introduced strict regulations for AI systems, with prohibitions on certain AI use cases which took effect in February 2025. On-premises deployment simplifies compliance. &lt;/p&gt;

&lt;p&gt;For financial organizations navigating complex regulatory environments, solutions like &lt;a href="https://www.actian.com/data-intelligence/platform/" rel="noopener noreferrer"&gt;Actian’s Data Intelligence platform&lt;/a&gt; helps enforce data governance and streamline compliance workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cloud Infrastructure Case Studies 37signals Validated
&lt;/h2&gt;

&lt;p&gt;As much as the financial transparency of 37signals’ cloud exit was radical, their repatriation was not an isolated occurrence. It was part of a growing trend by many organizations trying to regain cost control and optimize their cloud infrastructure. Many high-profile case studies illustrate the scale and economics of moving workloads back from public clouds to owned or hybrid infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dropbox
&lt;/h3&gt;

&lt;p&gt;Dropbox pioneered enterprise cloud repatriation as early as 2015, completing the migration between 2016 and 2018. &lt;a href="https://www.wired.com/2016/03/epic-story-dropboxs-exodus-amazon-cloud-empire/" rel="noopener noreferrer"&gt;The company moved roughly 90% of customer data&lt;/a&gt;, reportedly over 500 petabytes, off AWS to three owned colocation facilities. The infrastructure investment totaled $53 million, yet Dropbox reported $74.6 million in operational savings over two years per its &lt;a href="https://www.sec.gov/Archives/edgar/data/1467623/000119312518055809/d451946ds1.htm" rel="noopener noreferrer"&gt;2018 S‑1 filing&lt;/a&gt;. A small portion of workloads, primarily European customers and specialized services, remain in AWS. Internally, the initiative was known as “Magic Pocket,” and it exemplifies how a well-executed &lt;a href="https://www.actian.com/hybrid-cloud-architecture/" rel="noopener noreferrer"&gt;hybrid cloud approach&lt;/a&gt; can deliver substantial savings while aligning with long-term business objectives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ahrefs
&lt;/h3&gt;

&lt;p&gt;Ahrefs, the SEO tools company, relied on a Singapore colocation setup with 850 servers. Their reported savings from avoiding public cloud were approximately $400 million over 2.5 years. Actual infrastructure cost: $39.5 million for 850 servers (~$1,500/server/month), versus an estimated $447.7 million if hosted entirely on AWS (~$17,557/server/month equivalent). As &lt;a href="https://tech.ahrefs.com/how-ahrefs-saved-us-400m-in-3-years-by-not-going-to-the-cloud-8939dd930af8#:~:text=Ahrefs%20wouldn%E2%80%99t%20be%20profitable%2C%20or%20even%20exist%2C%20if%20our%20products%20were%20100%25%20on%20AWS." rel="noopener noreferrer"&gt;Ahrefs put it&lt;/a&gt;: “We wouldn’t be profitable, or even exist, if our products were 100% on AWS.” While critics argue that Ahrefs inflated AWS estimates, the directional savings were undeniable, illustrating that cloud repatriation challenges can be surmounted at scale with careful planning.&lt;/p&gt;

&lt;h3&gt;
  
  
  GEICO
&lt;/h3&gt;

&lt;p&gt;GEICO spent a decade migrating to multiple cloud providers only for its costs to climb and exceed projections by 2.5×, reaching $300 million by 2022 across eight providers. In response, GEICO began moving workloads to a private cloud using OpenStack and Kubernetes, targeting over 50% repatriation by 2029. &lt;a href="https://youtu.be/fKd5j6A7Nt0?si=ZISpHuQkRXpvdM9M" rel="noopener noreferrer"&gt;Early results&lt;/a&gt; show 50% reductions in compute and &lt;a href="https://www.thestack.technology/insurer-slashes-compute-costs-with-cloud-repatriation-shift-to-ocp-but/" rel="noopener noreferrer"&gt;60% reduction per gigabyte of storage costs&lt;/a&gt; compared with public cloud services, demonstrating how a hybrid cloud architecture can deliver efficiency, compliance, and alignment with long-term business objectives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Akamai
&lt;/h3&gt;

&lt;p&gt;Akamai was on the &lt;a href="https://www.linkedin.com/posts/tleighton_how-akamai-regained-control-of-its-runaway-activity-7282843623774142465-LR4c/" rel="noopener noreferrer"&gt;path to spending over $100 million&lt;/a&gt; on third party cloud services before migrating compute workloads to its own global edge network of 350,000+ servers. The migration delivered savings of roughly $100 million per year, a testament to the economics of repatriation when existing infrastructure and scale align.&lt;/p&gt;

&lt;p&gt;What these cases share is the same economic pattern documented by 37signals. Predictable, high-volume workloads eventually become cheaper to run on owned infrastructure than on hyperscaler clouds. &lt;/p&gt;

&lt;p&gt;These examples reflect a broader shift occurring across enterprise infrastructure strategies. &lt;a href="https://web.archive.org/web/20241123193805/https://substack-post-media.s3.us-east-1.amazonaws.com/post-files/152070619/7ea0372b-e710-4fcb-a41f-7f928549fb3d.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&amp;amp;X-Amz-Credential=ASIAUM3FPD6B2WS5FXT4%2F20241123%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20241123T193805Z&amp;amp;X-Amz-Expires=3600&amp;amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEIaCXVzLWVhc3QtMSJHMEUCIQC2kR555pFI8HmlydaNacbd9RPRu2BYjsS6BUtYIwpVmQIgIC5k5JRn0xlVZB2aZhNi0dpsodMbVuy0UiAYVgzoqYoqggQI2%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAEGgwzMDI0NzExMjY5MTUiDOeTbKFwEqOxR%2BttZCrWAwxAcmObVtjWpEZD5WO6PpOoZ3AHJwHU2Fi42m%2F6dZOWpMKZKr0gIvgXWjdexEN5NUFa5x2r4PiXbC%2FBEizoa4NEuUUVNHGSTQ9FB0rISgGfcqmZCi6L8aIT5IV%2FBRmwQQIwtZIJxVsDXdofktzdaeiMAa1DuA8ZWksfr2u936yuChersIvuBMcMw8izbrAbSL93AKYQhg5oxeBjagNc8%2BWKw1QLpkWeX4vonr%2F4Ili%2BuEJrchDRfQqJUkXK0vAi2zmeyRsNzA0OmhY3MKxZoanypr3D9v02mCrYmzvKTFd%2B0IfHAjDSTe%2FiUTfPxWnr%2BItyA9LNWyX7Fs4eYDPK6%2FvpVTEOlbkQWqUy5ukBPL6UDmmAT65x%2B7Ofhpr1CEMlBI8CiGc4gfbg4XBScKijmyp%2FTk%2FZD%2F%2BbzTohH8sxaRXot99gMt4DP2OQ7S5LJAoG5s6FsoD%2B5%2FeD0%2F2Gn6m4eIDmyh1LMwuKl99whqJTjybFAxGuK7SBfAtpz6%2Bg4e9VsA1eRFX8ckijgD80jFJOR7yLTFGxKNxmyoPgy2gAZWCgWO3Sp1Q0ixxphaXpg4v2F5CIqljcQwEMeUB9Ps%2F6iyHhtAUBANNmfpLqxlBMcM8Soo9GmXFOMNefiLoGOqUB58p6MbetrkYFc2LtsI9NvYH80vCh4fjHwaVTkvDIN5jmgiphqA6PwLPjv3It1nogoqYtk6snmGd58j%2FUNPkXYQpzQLp2olOZY1Ukb9HRgbYkcowP71cpDDTEL%2FryKutpNgx6WFBUhZmiJZL6t%2FGCXMNi8RbkC4dnr8mgTw3yburAGSK6mNKeLPLxjGlN%2FgOqlhjMX4GhWn%2BDuUpMzhNyvluMHFY6&amp;amp;X-Amz-Signature=c2a04d69b332738b503a91db32b88fa97ce966e59f4b536cc5dbbec0c7f15c78&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-disposition=attachment%3B%20filename%3D%22Barclays_Technology_2h24_Cio_Survey_2024.pdf%22&amp;amp;x-id=GetObject" rel="noopener noreferrer"&gt;Barclays’ Chief Information Officers (CIO) surveys&lt;/a&gt; show cloud repatriation trending upward in recent years, with the sentiment peaking in the second half of 2024 with 86% of CIOs planning repatriation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq188cccw2o7uu7s1gq2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq188cccw2o7uu7s1gq2q.png" alt="Figure 1: Barclay’s CIO survey showing 86% of CIOs planning cloud repatriation" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, this statistic does not mean that companies are abandoning public cloud environments completely. &lt;a href="https://www.idc.com/resource-center/blog/storm-clouds-ahead-missed-expectations-in-cloud-computing/#:~:text=only%208%2D9%25%20of%20companies%20plan%20full%20workload%20repatriation" rel="noopener noreferrer"&gt;According to IDC&lt;/a&gt;, only 8–9% of companies favor full repatriation with most preferring a hybrid approach that combines public and private clouds. Hybrid cloud infrastructure allows organizations to optimize workload placement by strategically allocating sensitive data and mission-critical applications on-premises while leveraging public cloud services for less critical workloads. As such, it has become increasingly important for teams exploring similar transitions to understand the nuances of &lt;a href="https://www.actian.com/blog/cloud-data-warehouse/hybrid-cloud-benefits-and-risks/" rel="noopener noreferrer"&gt;hybrid deployments and their associated risks.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud Repatriation Statistics
&lt;/h2&gt;

&lt;p&gt;Cloud repatriation is accelerating at the same time as &lt;a href="https://hostingjournalist.com/news/idc-public-cloud-spending-to-pass-1-trillion-in-2026" rel="noopener noreferrer"&gt;public cloud spending keeps climbing&lt;/a&gt;. IDC projects global public cloud spend will reach $1.6 trillion in 2028, doubling from their 2024 prediction. Yet as mentioned earlier, 86% of CIOs are planning some form of repatriation &lt;a href="https://www.databank.com/resources/blogs/why-86-of-cios-are-rethinking-their-cloud-strategy/" rel="noopener noreferrer"&gt;according to Barclays&lt;/a&gt;. Both trends can be true because this is not a cloud exodus so much as a rebalancing. Enterprises are leaning towards a hybrid cloud model.&lt;/p&gt;

&lt;p&gt;AI is likely to accelerate that shift. AI workloads account for less than 10% of total cloud compute today but &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2025-05-13-gartner-identifies-top-trends-shaping-the-future-of-cloud#:~:text=Gartner%20predicts%2050%25%20of%20cloud%20compute%20resources%20will%20be%20devoted%20to%20AI%20workloads%20by%202029" rel="noopener noreferrer"&gt;Gartner projects&lt;/a&gt; that this figure will approach 50% by 2029. Hyperscalers are responding with enormous capital investment. There is an &lt;a href="https://introl.com/blog/hyperscaler-capex-600b-2026-ai-infrastructure-debt-january-2026" rel="noopener noreferrer"&gt;estimated $600 billion in infrastructure spend&lt;/a&gt; in 2026, roughly three-quarters of it tied to AI. The assumption is clear: Enterprises will rent that GPU capacity. But the 37signals math suggests that once AI workloads move from experimentation to steady production, ownership economics begin to dominate.&lt;/p&gt;

&lt;p&gt;Cost pressure is already driving behavior. &lt;a href="https://info.flexera.com/CM-REPORT-State-of-the-Cloud" rel="noopener noreferrer"&gt;Flexera reports that 27%&lt;/a&gt; &lt;a href="https://info.flexera.com/CM-REPORT-State-of-the-Cloud" rel="noopener noreferrer"&gt;of cloud resources are wasted&lt;/a&gt; or underutilized, and &lt;a href="https://www.flexera.com/about-us/press-center/new-flexera-report-finds-84-percent-of-organizations-struggle-to-manage-cloud-spend" rel="noopener noreferrer"&gt;21% of workloads have already been repatriated&lt;/a&gt;. The primary reason cited is cost exceeding projections, followed by performance concerns. With GPUs, the margin for inefficiency is thinner. There are fewer optimization levers, higher hourly rates, and faster budget burn.&lt;/p&gt;

&lt;p&gt;Regulation adds another layer. The EU AI Act, DORA for financial services, China’s PIPL, and India’s DPDP are tightening data governance requirements. &lt;a href="https://www.mimecast.com/blog/why-data-sovereignty-is-now-a-dealbreaker-in-cybersecurity/" rel="noopener noreferrer"&gt;Mimecast reports&lt;/a&gt; that 87% of organizations now factor data sovereignty into vendor decisions. For AI systems, sovereignty extends beyond data location to model provenance, audit trails, and compliance documentation. On-premises deployment does not eliminate regulatory complexity, but it centralizes control and for many enterprises, that simplicity is becoming strategically attractive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fey79t7jet319mgg6zzjo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fey79t7jet319mgg6zzjo.png" alt="Figure 2: A bar chart showing why enterprises repatriate" width="800" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Counter-Arguments and When Cloud Providers Win
&lt;/h2&gt;

&lt;p&gt;Not all observers agree that cloud repatriation is the best path for every organization. Public cloud environments still deliver value in certain circumstances. But arguments often do not hold strong in the case of AI workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  When cloud wins vs. when on-premises wins
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Component&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Cloud&lt;/strong&gt; &lt;strong&gt;a&lt;/strong&gt;&lt;strong&gt;dvantage&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;On-&lt;/strong&gt;&lt;strong&gt;p&lt;/strong&gt;&lt;strong&gt;rem&lt;/strong&gt; &lt;strong&gt;a&lt;/strong&gt;&lt;strong&gt;dvantage&lt;/strong&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workload predictability&lt;/td&gt;
&lt;td&gt;Handles spiky or unpredictable workloads&lt;/td&gt;
&lt;td&gt;Predictable workloads cheaper to self-host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team expertise&lt;/td&gt;
&lt;td&gt;Requires minimal in-house infrastructure skill&lt;/td&gt;
&lt;td&gt;Strong IT teams can optimize and reduce vendor reliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale and growth&lt;/td&gt;
&lt;td&gt;Rapid scaling and global expansion&lt;/td&gt;
&lt;td&gt;Predictable growth enables cost-efficient hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulatory requirements&lt;/td&gt;
&lt;td&gt;Managed compliance, geo-redundancy&lt;/td&gt;
&lt;td&gt;Direct control simplifies regulatory alignment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost and margins&lt;/td&gt;
&lt;td&gt;Pay-as-you-go reduces upfront spend&lt;/td&gt;
&lt;td&gt;Long-term savings from owned infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service quality&lt;/td&gt;
&lt;td&gt;Cloud SLAs ensure availability and performance&lt;/td&gt;
&lt;td&gt;Dedicated resources guarantee predictable uptime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Cloud “wrong usage” argument
&lt;/h3&gt;

&lt;p&gt;Jeremy Daly, a serverless advocate, &lt;a href="https://www.jeremydaly.com/the-cloud-isnt-the-issue-youre-just-using-it-wrong/" rel="noopener noreferrer"&gt;argues that&lt;/a&gt; “37signals was using the cloud wrong.” By treating cloud environments as virtual colocation, running VMs and Kubernetes, they were paying cloud premiums without capturing the value of serverless, managed services, and instant scaling. As Daly notes, “In the cloud, we should be renting services, not servers.” &lt;/p&gt;

&lt;p&gt;For SaaS workloads with highly variable or spiky traffic, this argument is compelling. Serverless infrastructure allows organizations to scale instantly and pay only for the compute they actually use. &lt;/p&gt;

&lt;p&gt;However, AI inference workloads often behave very differently. Production inference systems, such as recommendation models, copilots, and document processing pipelines, tend to run at steady, sustained utilization rather than unpredictable bursts. In these cases, the economic advantage of elastic cloud scaling diminishes. The premium paid for burst capacity still exists, but the workload itself rarely needs that burst capacity.&lt;/p&gt;

&lt;p&gt;Daly’s argument therefore holds for variable SaaS workloads, where elasticity is critical. For sustained AI inference workloads running at high utilization, paying a premium for burst capacity that is rarely used can make dedicated infrastructure or hybrid deployments more cost-efficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full cost critique
&lt;/h3&gt;

&lt;p&gt;Some critics also question the financial assumptions behind 37signals’ approach. &lt;a href="https://wrld.tech/cloud-vs-on-premise-storage-a-cost-comparison-guide/#:~:text=Cloud%20storage%20is%20cost%2Deffective,%2C%20budget%2C%20and%20data%20requirements." rel="noopener noreferrer"&gt;They point out&lt;/a&gt; that hardware and software normally account for only about 20% of IT costs, with the remainder covering electricity, cooling, physical security, racking, Uninterruptible Power Supply (UPS), and opportunity costs. David Heinemeier Hanson’s analysis did not include all of these overheads because 37signals used colocation facilities rather than fully owned data centers. Even so, considering 37signals’ figures, it is reasonable to conclude that renting colocation space can still be far cheaper than relying on cloud services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Competence vs. growth framework
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://newsletter.goodtechthings.com/p/wait-is-cloud-bad" rel="noopener noreferrer"&gt;Forrest Brazeal’s IT competence versus growth aspirations framework&lt;/a&gt; provides additional nuance. He places 37signals in the &lt;em&gt;High Competence/Low Growth&lt;/em&gt; quadrant, ideal for self-hosting. &lt;em&gt;“Not every company has the competence (high) or growth aspirations (low) of 37signals,”&lt;/em&gt; he observes. Startups with uncertain or spiky workloads benefit from cloud flexibility, but AI companies running production inference at scale often combine high operational competence with steady growth. Such profiles (steady growth &amp;amp; high competence) are well suited to repatriation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the Playbook to AI Infrastructure
&lt;/h2&gt;

&lt;p&gt;If 37signals provided the economic blueprint, AI infrastructure makes the economics more concrete. The decision is no longer abstract. It becomes a structured assessment grounded in workload behavior, utilization, and regulatory exposure.&lt;/p&gt;

&lt;p&gt;A practical four-question framework helps translate the 37signals logic into AI terms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is your inference workload predictable and sustained?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Unlike SaaS traffic spikes, most production AI systems such as recommendation engines, RAG pipelines, or fraud detection models process steady volumes with gradual growth.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are projected GPU utilization rates above 60–70%?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
At this threshold, owned hardware amortization typically undercuts public cloud GPU pricing within the first year.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are you processing more than 10–50 million queries per month?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
At this scale, per-token and per-query pricing from cloud APIs compound rapidly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do you face data sovereignty or strict compliance requirements?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For financial services, healthcare, or government workloads, regulatory mandates can tilt the decision toward controlled environments.&lt;/p&gt;

&lt;p&gt;If the answer is “yes” to three or four of these, the repatriation economics tend to favor on-premises deployment for production inference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision matrix
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;strong&gt;Workload&lt;/strong&gt; &lt;strong&gt;s&lt;/strong&gt;&lt;strong&gt;tage&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Recommended&lt;/strong&gt; &lt;strong&gt;e&lt;/strong&gt;&lt;strong&gt;nvironment&lt;/strong&gt;
&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Rationale&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model training&lt;/td&gt;
&lt;td&gt;Public cloud&lt;/td&gt;
&lt;td&gt;Compute-intensive; cloud GPUs handle burst workloads cost-effectively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimentation and prototyping&lt;/td&gt;
&lt;td&gt;Public cloud&lt;/td&gt;
&lt;td&gt;Flexible, fast provisioning for early-stage iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production inference&lt;/td&gt;
&lt;td&gt;On-premises / Hybrid&lt;/td&gt;
&lt;td&gt;Steady workloads; owned hardware cheaper at 60–70%+ GPU utilization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector storage (embeddings)&lt;/td&gt;
&lt;td&gt;On-premises&lt;/td&gt;
&lt;td&gt;Reduces recurring managed-service costs and ensures data control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The hybrid AI pattern
&lt;/h3&gt;

&lt;p&gt;In practice, most AI organizations adopt a hybrid model rather than an all-or-nothing shift. Training remains in the cloud. Inference moves closer to owned infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lenovopress.lenovo.com/lp2225-on-premise-vs-cloud-generative-ai-total-cost-of-ownership-2025-edition#:~:text=will%20run%20you%20over%20%24483%20M%20in%20cloud%20costs" rel="noopener noreferrer"&gt;Lenovo documented&lt;/a&gt; that training Llama 3.1 at hyperscale (39.3 million GPU hours) in the cloud would exceed $483 million. That type of elastic, short-term scale is exactly where public cloud excels. Inference is different. Once a model is trained, serving it for three to five years becomes steady, predictable work. That is where amortized hardware economics has the upper hand.&lt;/p&gt;

&lt;p&gt;This split architecture also simplifies data migration risk. Instead of relocating entire AI pipelines at once, organizations can migrate production inference workloads gradually while leaving experimentation and early-stage training in cloud environments. A controlled, phased migration process reduces operational disruption while ensuring seamless integration between cloud-based training and on-premises serving layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-hosted inference economics
&lt;/h3&gt;

&lt;p&gt;The economics of self-hosted inference depend heavily on utilization and token volume. According to enterprise deployment benchmarks, a 7B-parameter model running on an H100 GPU at roughly 70% utilization &lt;a href="https://blog.premai.io/private-llm-deployment-a-practical-guide-for-enterprise-teams-2026/" rel="noopener noreferrer"&gt;costs about $10,000 per year in spot nodes&lt;/a&gt; or hardware amortization. Power costs about $300 annually, bringing the total costs to about $10,300.&lt;/p&gt;

&lt;p&gt;Public LLM APIs, by contrast, typically charge per million tokens, with enterprise pricing in 2025 ranging from $0.25–$15 per million input tokens and $1.25–$75 per million output tokens depending on model tier and provider. &lt;/p&gt;

&lt;p&gt;At low usage levels, APIs remain the more economical option because infrastructure sits idle. However, the economics change as workloads scale. Industry analyses suggest that self-hosted deployment begins to break even at roughly two million tokens per day, after which the fixed cost of owned infrastructure is amortized across a large inference volume.&lt;/p&gt;

&lt;p&gt;At high volumes, self-hosted inference can reduce costs by up to 78%. &lt;a href="https://medium.com/artefact-engineering-and-data-science/llms-deployment-a-practical-cost-analysis-e0c1b8eb08ca#:~:text=8k%3C%20conversations%20per%20day" rel="noopener noreferrer"&gt;Artefact’s analysis&lt;/a&gt;) found break-even around 8,000 conversations per day. Below that threshold, managed cloud APIs remain more economical. Above it, ownership compounds savings. The pattern mirrors 37signals: predictable workload plus high utilization equals rapid payback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector databases
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.instacart.com/company/tech-innovation/how-instacart-built-a-modern-search-infrastructure-on-postgres" rel="noopener noreferrer"&gt;Instacart documented&lt;/a&gt; migrating from Elasticsearch plus FAISS to PostgreSQL with pgvector, achieving 80% cost savings and a 10× reduction in write amplification. Timescale’s pgvectorscale benchmarks show &lt;a href="https://www.tigerdata.com/blog/pgvector-is-now-as-fast-as-pinecone-at-75-less-cost" rel="noopener noreferrer"&gt;approximately 75% lower costs&lt;/a&gt; than managed vector services like Pinecone at comparable performance.&lt;/p&gt;

&lt;p&gt;For RAG systems handling millions of queries monthly, self-hosted vector infrastructure produces savings that resemble the 37signals S3 case: large recurring storage bills replaced by amortized hardware and open-source tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data sovereignty as a structural driver
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.grandviewresearch.com/industry-analysis/sovereign-cloud-market-report#:~:text=size%20was%20estimated%20at%20USD%2096.77%20billion%20in%202024%20and%20is%20projected%20to%20reach%20USD%20648.87%20billion%20by%202033%2C%20growing%20at%20a%20CAGR%20of%2023.8%25%20from%202025%20to%202033" rel="noopener noreferrer"&gt;Grandview research&lt;/a&gt; reports that the sovereign cloud market was worth 648.87 billion USD in 2025 and is projected to reach USD 648.87 billion by 2033. Also, &lt;a href="https://www.n-ix.com/data-sovereignty/#:~:text=By%202028%2C%20industry%20forecasts%20suggest%20that%2060%25%20of%20financial%20services%20firms%20outside%20the%20US%20will%20adopt%20sovereign%20cloud%20environments%20to%20comply%20with%20DORA%20and%20related%20data%20sovereignty%20regulations%20%5B2%5D." rel="noopener noreferrer"&gt;according to&lt;/a&gt; &lt;a href="https://www.n-ix.com/data-sovereignty/#:~:text=By%202028%2C%20industry%20forecasts%20suggest%20that%2060%25%20of%20financial%20services%20firms%20outside%20the%20US%20will%20adopt%20sovereign%20cloud%20environments%20to%20comply%20with%20DORA%20and%20related%20data%20sovereignty%20regulations%20%5B2%5D." rel="noopener noreferrer"&gt;Gartner&lt;/a&gt;, around 60% of financial firms outside the United States are expected to adopt sovereign or on-premises deployments by 2028.&lt;/p&gt;

&lt;p&gt;Frameworks such as the EU AI Act, China’s PIPL, and India’s DPDP mandate data localization and traceability. For organizations processing sensitive training datasets or proprietary inference logs, on-premises deployment inherently satisfies residency requirements because data never leaves jurisdictional boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;37signals showed that cloud repatriation teams can measure, model, and defend decisions with hard numbers. With AI infrastructure, the economics can be even more pronounced. If cloud repatriation saved roughly $10 million for Basecamp, an equivalent AI company running production inference at comparable scale could save multiples of that amount, given the much higher cost of GPU compute and embedding infrastructure.&lt;/p&gt;

&lt;p&gt;For organizations choosing to run AI workloads in controlled environments, platforms like &lt;a href="https://www.actian.com/databases/vectorai-db/" rel="noopener noreferrer"&gt;Actian VectorAI DB&lt;/a&gt; provide a purpose-built vector database designed for high-volume vector search and AI inference workloads. It can be deployed on-premises or in the cloud, allowing organizations to place vector infrastructure where it best fits their operational and economic requirements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://discord.gg/432A2M63Py" rel="noopener noreferrer"&gt;Join the community&lt;/a&gt; and learn more about Actian.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>cloud</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>How to Build a HIPAA Compliant AI Ecosystem Without the Cloud</title>
      <dc:creator>Offisong Emmanuel</dc:creator>
      <pubDate>Tue, 19 May 2026 19:42:41 +0000</pubDate>
      <link>https://dev.to/actiandev/how-to-build-a-hipaa-compliant-ai-ecosystem-without-the-cloud-2dih</link>
      <guid>https://dev.to/actiandev/how-to-build-a-hipaa-compliant-ai-ecosystem-without-the-cloud-2dih</guid>
      <description>&lt;p&gt;Healthcare cannot rely on cloud RAG because patient data leaves your network and your system logs, stores, and exposes it outside your control. You sign a Business Associate Agreement (BAA), connect your pipeline to a managed vector database, and assume compliance is complete. That assumption is wrong. The BAA covers the provider’s infrastructure. It does not cover what your application sends, logs, or exposes during retrieval and generation.&lt;/p&gt;

&lt;p&gt;You remain responsible for every path your system sends Protected Health Information (PHI) through. A clinician query can leak sensitive data through logs. A system prompt can include patient context that your system stores outside your boundary. Weak access control allows retrieval results to expose records across departments. These risks exist in your application layer, not in the cloud provider’s scope.&lt;/p&gt;

&lt;p&gt;American regulators now target this gap. In 2026, they &lt;a href="https://www.hakunamatatatech.com/our-resources/blog/hipaa-compliant-llm" rel="noopener noreferrer"&gt;flagged attack patterns like membership inference&lt;/a&gt;, where an adversary probes an AI system to confirm whether a patient’s data exists in the index. Cloud-hosted pipelines increase this risk because queries and embeddings move across external infrastructure. Audit requirements tighten further when logs live on third-party systems.&lt;/p&gt;

&lt;p&gt;In this tutorial, you will build a clinical knowledge assistant that runs entirely on hospital infrastructure. It performs semantic search over clinical data, enforces role-based access at query time, and generates answers with clear citations. Every query stays inside your network, every access is logged locally, and no external API calls are required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why BAA Is Not Enough
&lt;/h2&gt;

&lt;p&gt;A BAA protects the cloud provider’s infrastructure, not how your system handles PHI during queries, retrieval, and generation. You remain responsible for every place PHI appears, moves, or gets stored inside your pipeline. There are multiple failure modes that make your system non-compliant, even when you sign a BAA.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Shared responsibility gap&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The BAA stops at the infrastructure boundary. Your system controls what enters a prompt, what gets logged, and what leaves your network. If a clinician query includes PHI and your application logs it to an external service, you are responsible. If your retrieval step returns records across departments without strict filters, you have created an internal data breach. These failures happen in your code, not in the cloud provider’s scope.&lt;/p&gt;

&lt;p&gt;For example, a physician searches “Show me similar cases to John Doe with early stage lung cancer.” Your application logs the full query to a cloud logging service for debugging. That log now contains PHI outside your network. The cloud provider did not leak it. Your application sent it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Audit log ownership&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://conciergehealthcareattorneysllc.com/blog/hipaa-audit-trail-requirements-what-healthcare-practitioners-need-to-know/" rel="noopener noreferrer"&gt;HIPAA requires a complete audit trail&lt;/a&gt; for every access to PHI. When your vector database runs on third-party infrastructure, your system stores query logs and retrieval traces outside your control. You cannot guarantee completeness, retention, or isolation. Your security team cannot verify access patterns without relying on another provider’s system. That breaks your ability to enforce and prove compliance.&lt;/p&gt;

&lt;p&gt;For example, your compliance team asks for a report of all oncology patient records in the past 30 days. Your vector database provider stores query logs on their platform with limited retention. Some logs are missing and others lack user-level metadata. You cannot produce a complete audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Membership inference exposure&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Attackers can probe your system with targeted queries to determine whether a specific patient’s data exists in your index. This attack class is now a regulatory concern. Cloud-hosted indexes increase this risk because they expose a remote interface for repeated probing. A locally hosted index removes that external interface and limits access to your internal network.&lt;/p&gt;

&lt;p&gt;For example, an attacker sends repeated queries like “Patients diagnosed with HIV in 2024 treated with drug X” and slightly modifies filters each time. They observe changes in response confidence and content. Over time, they infer whether a specific individual’s record exists in your dataset.&lt;/p&gt;

&lt;p&gt;These failures show that a BAA does not ensure compliance. An on-premises deployment &lt;a href="https://www.actian.com/blog/databases/when-to-choose-on-premises-vs-cloud-for-vector-databases/" rel="noopener noreferrer"&gt;removes the third-party surface entirely&lt;/a&gt; and gives you full control over data flow, access, and auditability.&amp;nbsp;&lt;/p&gt;

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

&lt;p&gt;Split view showing Cloud vs On-premises RAG architecture&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Are Building
&lt;/h2&gt;

&lt;p&gt;In this section, you will build a RAG system with three layers:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Ingestion layer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;You ingest clinical notes and treatment protocols into a controlled vector index with enforced data hygiene. You de-identify data before any processing. HIPAA Safe Harbor requires removal of identifiers, while Expert Determination allows a statistical approach. You apply one of these before ingestion, not after. You then chunk documents into 512 token segments with 50 token overlap, generate embeddings using a local model, and store them in VectorAI DB with metadata.&lt;/p&gt;

&lt;p&gt;You define a strict schema for every record. Each chunk includes document type, department, date, and author role. This metadata is not optional. It enables access control at query time and prevents cross-department leakage. Do not store raw documents without structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Query layer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;You process clinician queries through a controlled retrieval pipeline. Every query passes through role-based access control before it reaches the index. A cardiology user can only retrieve cardiology data. A scheduling bot cannot access diagnosis notes. You enforce this with a MUST filter on department or patient cohort at the database level.&lt;/p&gt;

&lt;p&gt;Run hybrid search. Vector similarity retrieves semantically relevant chunks. Metadata filters restrict the result set. Pass the filtered context into a local LLM. The model generates an answer from retrieved data only and includes citations. Do not allow the model to invent or pull from external knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Audit layer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Log every interaction locally with full traceability. Each query writes a record that includes timestamp, user ID, department, query text, and retrieved document references. This log lives on your infrastructure with defined retention and access policies. You do not rely on external logging systems.&lt;/p&gt;

&lt;p&gt;You can reconstruct any access event from this log. You can answer who accessed what, when, and under which role. This satisfies audit requirements and gives your security team direct visibility into system behavior.&lt;/p&gt;

&lt;p&gt;The entire system runs on commodity hardware inside the hospital network. The end-to-end architecture of the system is shown in the image:&lt;/p&gt;

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

&lt;p&gt;Hospital RAG system architecture&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a HIPAA Compliant RAG Workflow
&lt;/h2&gt;

&lt;p&gt;In this section, you will build a fully local RAG system that ingests clinical data, enforces access control, answers queries, and logs every interaction.&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Prerequisites&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To follow along, install the following tools on your local network:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Docker and Docker Compose installed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Python 3.10 or higher.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://pypi.org/project/pip/" rel="noopener noreferrer"&gt;PIP&lt;/a&gt; or &lt;a href="https://docs.astral.sh/uv/getting-started/installation/" rel="noopener noreferrer"&gt;UV&lt;/a&gt;: This guide uses UV.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 1: Deploy a vector database&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;You deploy a local instance of &lt;a href="https://www.actian.com/databases/vectorai-db/" rel="noopener noreferrer"&gt;Actian VectorAI DB&lt;/a&gt; with persistent storage for both vector data and audit logs.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;services:
  vectorai:
    image: williamimoh/actian-vectorai-db:latest
    platform: linux/amd64
    container_name: vectorai_db
    ports:
      - &lt;span class="s2"&gt;"50052:50051"&lt;/span&gt;
    volumes:
      &lt;span class="c"&gt;# vector data persists across restarts&lt;/span&gt;
      - ./data:/app/data
      &lt;span class="c"&gt;# audit log lives on host — not inside the container&lt;/span&gt;
      - ./audit_logs:/app/audit_logs
    environment:
      - &lt;span class="nv"&gt;VECTORAI_LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info
    restart: unless-stopped

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

&lt;/div&gt;



&lt;p&gt;Run the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database starts and exposes port 50051 for local access. Vector data persists in ./data. Audit logs write to &lt;code&gt;./audit_logs&lt;/code&gt; on the host, which keeps all access records inside your network boundary.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt;&amp;nbsp;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;M3/_M4 Apple Silicons might encounter a GRPC disconnection error without any container _logs. In this case, disable Rosetta in Docker Desktop.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;VectorAI DB is under active development.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 2: Build the ingestion pipeline&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Install the client library and run the ingestion pipeline to convert clinical documents into embeddings and store them in your local vector database.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;uv&lt;/strong&gt; for dependency management and execution. It is fast, reproducible, and avoids global Python state.&lt;/p&gt;

&lt;p&gt;Download the &lt;a href="https://github.com/hackmamba-io/actian-vectorAI-db-beta/blob/main/actian_vectorai-0.1.0b2-py3-none-any.whl" rel="noopener noreferrer"&gt;Actian VectorAI client package&lt;/a&gt;. This creates a file &lt;code&gt;actian_vectorai-0.1.0b2-py3-none-any&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Initialize your project by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv init .
uv venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After initialization, install the Actian VectorAI package by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;uv&lt;/span&gt; &lt;span class="n"&gt;pip3&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;actian_vectorai&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;py3&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;none&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;any&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the embedding model dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;uv&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;transformers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file &lt;code&gt;ingest.py&lt;/code&gt; with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;actian_vectorai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VectorAIClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PointStruct&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="c1"&gt;# ── Config ────────────────────────────────────────────────────────────────────
&lt;/span&gt;&lt;span class="n"&gt;VECTORAI_HOST&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:50052&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;COLLECTION&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clinical_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# 384-dim
&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DIM&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;384&lt;/span&gt;
&lt;span class="n"&gt;CHUNK_TOKENS&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;
&lt;span class="n"&gt;OVERLAP_TOKENS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;

&lt;span class="c1"&gt;# ── Synthetic clinical notes (replace with real de-identified corpus) ─────────
&lt;/span&gt;&lt;span class="n"&gt;RAW_NOTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card_note_001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clinical_note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cardiology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-03-15&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attending_physician&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            Patient: [NAME REDACTED], DOB: [DATE REDACTED], MRN: [MRN REDACTED]
            Chief Complaint: Chest pain radiating to left arm, onset 2 hours ago.
            Assessment: Acute ST-elevation myocardial infarction confirmed on ECG.
            History: Hypertension and type 2 diabetes. Started on aspirin 325 mg,
            clopidogrel 600 mg loading dose, and heparin infusion per ACS protocol.
            Plan: Emergency PCI. Beta-blocker therapy with metoprolol succinate
            25 mg daily post-procedure. ACE inhibitor ramipril 5 mg daily initiated
            24 hours post-PCI. Follow-up echocardiography in 6 weeks.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card_protocol_001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;treatment_protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cardiology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-01-10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department_head&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            Cardiology Protocol — Heart Failure with Reduced EF (HFrEF)
            First-line therapy:
            - ACE inhibitor: ramipril 2.5–10 mg daily (or ARB if ACE-intolerant).
            - Beta-blocker: bisoprolol 1.25–10 mg daily, carvedilol 3.125–25 mg BID,
              or metoprolol succinate 12.5–200 mg daily. Titrate every 2 weeks.
            - MRA: spironolactone 25–50 mg daily for NYHA class II–IV
              if eGFR &amp;gt; 30 and K+ &amp;lt; 5.0.
            Target: Symptomatic improvement. Reassess LVEF at 3–6 months.
            Device therapy (ICD/CRT) if LVEF ≤ 35% after 3 months optimal therapy.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;psych_note_001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clinical_note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;psychiatry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-03-18&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;psychiatrist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            Psychiatry intake note — [NAME REDACTED], [AGE REDACTED]-year-old.
            Presenting with major depressive episode, PHQ-9 score 18 (severe).
            No current suicidal ideation. Started sertraline 50 mg daily.
            Psychotherapy referral placed. Follow-up in 2 weeks.
            Safety plan documented. Family support confirmed present.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;onco_note_001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clinical_note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oncology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-03-20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oncologist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            Oncology note — [NAME REDACTED].
            Diagnosis: Stage IIIA non-small cell lung cancer, adenocarcinoma.
            EGFR mutation positive (exon 19 deletion).
            Plan: Osimertinib 80 mg daily (first-line EGFR-targeted therapy).
            Baseline CT chest/abdomen/pelvis completed. Brain MRI negative.
            Next imaging review in 8 weeks. Antiemetics PRN, skin care for rash.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# ── Step 1: De-identification ─────────────────────────────────────────────────
# For production use Presidio:
#   from presidio_analyzer import AnalyzerEngine
#   from presidio_anonymizer import AnonymizerEngine
#   analyzer, anonymizer = AnalyzerEngine(), AnonymizerEngine()
#   result = analyzer.analyze(text=raw, entities=[...], language="en")
#   clean  = anonymizer.anonymize(text=raw, analyzer_results=result).text
#
# This demo applies lightweight regex to already-synthetic notes.
&lt;/span&gt;
&lt;span class="n"&gt;_HIPAA_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b\d{3}-\d{2}-\d{4}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[SSN]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# SSN
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bMRN[-:\s]*\d{4,10}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[MRN]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# medical record #
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b\d{1,2}/\d{1,2}/\d{2,4}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DATE]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# dates
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b[A-Z][a-z]+ [A-Z][a-z]+\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[NAME]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# names (simple)
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b\d{3}[-.\s]\d{3}[-.\s]\d{4}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[PHONE]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# phone
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.\w+\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[EMAIL]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;# email
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b\d{5}(?:-\d{4})?\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[ZIP]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# zip
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(?:https?://)\S+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[URL]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# URLs
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[IP]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# IP addresses
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deidentify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Remove HIPAA Safe Harbor identifiers from text.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_HIPAA_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# ── Step 2: Chunking ──────────────────────────────────────────────────────────
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CHUNK_TOKENS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OVERLAP_TOKENS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Split text into overlapping token windows (whitespace tokenisation).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;

&lt;span class="c1"&gt;# ── Step 3: Embedding ─────────────────────────────────────────────────────────
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading embedding model: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# ── Step 4: Ingest into VectorAI DB ──────────────────────────────────────────
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_chunk_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Stable integer ID from (document_id, chunk_index).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;VectorAIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTORAI_HOST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Health check
&lt;/span&gt;        &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;health_check&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VectorAI DB connected  version=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Create collection (skip if already exists)
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLLECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;vectors_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DIM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cosine&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Collection &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;COLLECTION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; created  dim=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DIM&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exists&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Collection &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;COLLECTION&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; already exists — skipping create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;

        &lt;span class="n"&gt;total_chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# De-identify FIRST — before chunking or embedding
&lt;/span&gt;            &lt;span class="n"&gt;clean_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deidentify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

            &lt;span class="c1"&gt;# Chunk second
&lt;/span&gt;            &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Embed third
&lt;/span&gt;            &lt;span class="n"&gt;vectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Build PointStruct records with strict metadata schema
&lt;/span&gt;            &lt;span class="c1"&gt;# All four metadata fields are REQUIRED — no optional fields.
&lt;/span&gt;            &lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="nc"&gt;PointStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_chunk_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="c1"&gt;# ── strict schema ──────────────────────────────────────
&lt;/span&gt;                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# required
&lt;/span&gt;                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;       &lt;span class="c1"&gt;# required — RBAC filter key
&lt;/span&gt;                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;             &lt;span class="c1"&gt;# required
&lt;/span&gt;                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;# required
&lt;/span&gt;                        &lt;span class="c1"&gt;# ── retrieval helpers ──────────────────────────────────
&lt;/span&gt;                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                &lt;span class="c1"&gt;# de-identified chunk text
&lt;/span&gt;                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COLLECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;total_chunks&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  ✓ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  dept=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  chunks=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Ingestion complete — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; documents, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_chunks&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chunks total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RAW_NOTES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This file performs the following actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;De-identifies data:&lt;/strong&gt; The system removes all HIPAA identifiers from raw text before processing. Names, dates, and other sensitive fields are replaced with placeholders to prevent PHI from entering the system pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chunks the texts:&lt;/strong&gt; The system splits the cleaned text into 512-token segments with a 50-token overlap. This overlap preserves context across boundaries, enhancing retrieval accuracy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embeds the chunks:&lt;/strong&gt; The model converts each chunk into a numerical vector using a local sentence-transformers model. This process captures semantic meaning while keeping all processing within the network.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stores with metadata:&lt;/strong&gt; The system writes each chunk and its vector to VectorAI DB, along with necessary fields like document_type, department, date, and author_role. These fields support strict access control during queries.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the script by:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;uv run ingest.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You should see the following results after running the command:&lt;/p&gt;

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

&lt;p&gt;ingest.py execution&lt;/p&gt;

&lt;p&gt;From the logs, you see that the ingestion pipeline writes chunks to VectorAI DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 3: Run your queries&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Execute queries against your local RAG system and validate retrieval, access control, and audit logging.&lt;/p&gt;

&lt;p&gt;Create a file &lt;code&gt;query.py&lt;/code&gt; with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;actian_vectorai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VectorAIClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;actian_vectorai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FilterBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="c1"&gt;# ── Config ─────────────────────────────────────────────────────────────────────
&lt;/span&gt;&lt;span class="n"&gt;VECTORAI_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:50052&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;COLLECTION&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clinical_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;AUDIT_LOG&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./audit_logs/queries.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# volume-mounted path
&lt;/span&gt;
&lt;span class="c1"&gt;# Ollama settings — set OLLAMA_ENABLED=True once `ollama serve` is running
&lt;/span&gt;&lt;span class="n"&gt;OLLAMA_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;          &lt;span class="c1"&gt;# flip to True when Ollama is ready
&lt;/span&gt;&lt;span class="n"&gt;OLLAMA_URL&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;OLLAMA_MODEL&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mistral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;      &lt;span class="c1"&gt;# or "llama3.2:3b" for lower hardware
&lt;/span&gt;
&lt;span class="c1"&gt;# ── RBAC: role → allowed departments ──────────────────────────────────────────
# Access is enforced as a MUST filter at the database level.
# A scheduling_bot cannot reach clinical notes; cardiology cannot see psychiatry.
&lt;/span&gt;&lt;span class="n"&gt;ROLE_PERMISSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cardiology_clinician&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cardiology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oncology_clinician&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oncology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general_practitioner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cardiology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oncology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cardiology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oncology&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;psychiatry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scheduling_bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scheduling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# no clinical note access
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccessDeniedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allowed_departments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ROLE_PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AccessDeniedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown role &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; — access denied by default.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ROLE_PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# ── Embedding (reuse the same model as ingest.py) ─────────────────────────────
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading embedding model: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# ── Step 5: Search with department MUST filter ─────────────────────────────────
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_vec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;departments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Hybrid retrieval: vector similarity + metadata MUST filter.
    Results from departments outside the allowed list are impossible —
    the filter is applied at the database level, not in application code.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;VectorAIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTORAI_HOST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dept&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;departments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COLLECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="c1"&gt;# MUST filter — department equality enforced at DB level
&lt;/span&gt;                &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FilterBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;must&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dept&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# ── Step 6: LLM answer via Ollama ─────────────────────────────────────────────
&lt;/span&gt;&lt;span class="n"&gt;_RAG_SYSTEM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a clinical decision support assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer ONLY using the context passages below. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Do NOT use external knowledge or make assumptions. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cite each fact as [Doc N]. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If the context is insufficient, say: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;I cannot answer from the available documents.&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Doc &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, dept=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, role=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;author_role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;OLLAMA_ENABLED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Return raw retrieved context when LLM is disabled
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[LLM disabled — set OLLAMA_ENABLED=True]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_RAG_SYSTEM&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Context:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Answer:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_predict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;OLLAMA_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Ollama unreachable: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Retrieved context:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;AUDIT_LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AUDIT_LOG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ── Public query entry point ───────────────────────────────────────────────────
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Execute a role-gated RAG query.

    Returns:
        {answer, retrieved_docs, access_denied, error}
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# RBAC check — before anything else
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;departments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;allowed_departments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;AccessDeniedError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;write_audit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DENIED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_provided&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;denial_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access denied: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Embed → retrieve (with MUST filter) → generate
&lt;/span&gt;    &lt;span class="n"&gt;q_vec&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;departments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;doc_refs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Audit log — every query, regardless of outcome
&lt;/span&gt;    &lt;span class="nf"&gt;write_audit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;departments&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;doc_refs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer_provided&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc_refs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ── Demo runs ─────────────────────────────────────────────────────────────────
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="n"&gt;separator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;─&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;

    &lt;span class="c1"&gt;# ── Query 1: authorised cardiology query ────────────────────────────────
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QUERY 1 — cardiology_clinician (authorised)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dr_chen_007&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cardiology_clinician&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What beta-blocker is recommended for heart failure with reduced ejection fraction?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Answer:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retrieved sources:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;document_type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]  dept=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;document_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  chunk=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chunk_index&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# ── Query 2: scheduling bot tries to access clinical notes ───────────────
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QUERY 2 — scheduling_bot (attempting clinical note access)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot_sched_01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scheduling_bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What are the diagnosis notes for cardiology patients?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;✗ Access denied (as expected): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Answer:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sources:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# ── Query 3: cardiology query that must NOT return psychiatry notes ───────
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QUERY 3 — cardiology_clinician (RBAC must exclude psychiatry)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dr_chen_007&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cardiology_clinician&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;antidepressant dosing and patient management&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;departments_returned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r3&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="n"&gt;cross_leak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;psychiatry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;departments_returned&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Departments in results: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;departments_returned&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cross-department leak: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;✗ LEAK DETECTED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cross_leak&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;✓ none — RBAC working correctly&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# ── Show audit log tail ───────────────────────────────────────────────────
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUDIT LOG  →  {AUDIT_LOG}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;AUDIT_LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AUDIT_LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;          &lt;span class="c1"&gt;# show last 3 entries
&lt;/span&gt;            &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs_accessed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieved_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No audit log found — run ingest.py first.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script performs three core operations in a single flow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enforces access control&lt;/strong&gt;: The system checks the user’s role before retrieving any data. Each role is mapped to specific departments, and enforced as a mandatory filter at the database level. The authorization layer immediately blocks and logs unauthorized roles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieve and generate answers:&lt;/strong&gt; The system embeds the query and retrieves relevant document chunks using vector search, with strict department filters applied. The results are then passed to a local LLM. If the LLM is disabled, the retrieved context is returned directly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write audit logs:&lt;/strong&gt; The system logs every query locally, including the user ID, role, query text, accessed documents, and access status. This creates a complete audit trail for compliance and review.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the script by:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;uv run query.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You should see the following results after running the command:&lt;/p&gt;

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

&lt;p&gt;query.py execution&lt;/p&gt;

&lt;p&gt;The output shows three test cases: one showing a valid clinician query, a denied access attempt, and a check for cross-department leakage. These confirm that RBAC and audit logging work correctly before you move to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 4: Configure the audit log&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Store every query locally by using the volume mapping defined during deployment.&lt;/p&gt;

&lt;p&gt;The Docker configuration mounts ./audit_logs from your host into the container. When you run queries, this creates a local folder named audit_logs with a file queries.jsonl.&lt;/p&gt;

&lt;p&gt;The file contains the following entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-31T15:56:02.552088+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dr_chen_007"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardiology_clinician"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"department"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardiology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What beta-blocker is recommended for heart failure with reduced ejection fraction?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer_provided"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"access_denied"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-31T15:56:03.098346+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bot_sched_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scheduling_bot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"department"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scheduling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What are the diagnosis notes for cardiology patients?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer_provided"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"access_denied"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-31T15:56:03.663767+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dr_chen_007"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardiology_clinician"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"department"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardiology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"antidepressant dosing and patient management"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer_provided"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"access_denied"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-31T15:56:18.331657+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dr_chen_007"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardiology_clinician"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"department"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardiology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What beta-blocker is recommended for heart failure with reduced ejection fraction?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer_provided"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"access_denied"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-31T15:56:18.492188+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bot_sched_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scheduling_bot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"department"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scheduling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What are the diagnosis notes for cardiology patients?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer_provided"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"access_denied"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-31T15:56:18.569824+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dr_chen_007"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardiology_clinician"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"department"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardiology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"antidepressant dosing and patient management"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retrieved_docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"answer_provided"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"access_denied"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line represents a single query event. The log captures who made the request, their role, the department scope, the query text, and whether access was allowed. This file lives entirely on your infrastructure and gives you a complete, verifiable audit trail for every interaction with PHI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;A BAA was never enough because it does not control how your application handles PHI. You solved that by keeping all data, queries, and logs inside your network.&lt;/p&gt;

&lt;p&gt;You now have a RAG system that enforces role-based access, retrieves only authorized data, and logs every interaction locally, without external APIs or third-party exposure.&lt;/p&gt;

&lt;p&gt;Apply this pattern to other regulated systems. Refer to &lt;a href="https://docs.vectoraidb.actian.com/" rel="noopener noreferrer"&gt;the&lt;/a&gt; &lt;a href="https://actianvectorai.mintlify.app/home/getting-started/overview" rel="noopener noreferrer"&gt;VectorAI DB documentation&lt;/a&gt; and &lt;a href="https://github.com/actiancorp" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; for updates and implementation details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://discord.gg/432A2M63Py" rel="noopener noreferrer"&gt;Join the community&lt;/a&gt; and learn more about Actian.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>privacy</category>
      <category>rag</category>
      <category>security</category>
    </item>
    <item>
      <title>When to choose on-premises vs. cloud for vector databases</title>
      <dc:creator>Offisong Emmanuel</dc:creator>
      <pubDate>Tue, 19 May 2026 19:42:27 +0000</pubDate>
      <link>https://dev.to/actiandev/when-to-choose-on-premises-vs-cloud-for-vector-databases-2bi1</link>
      <guid>https://dev.to/actiandev/when-to-choose-on-premises-vs-cloud-for-vector-databases-2bi1</guid>
      <description>&lt;p&gt;For most of the last decade, the on-premises vs. cloud debate felt settled. Cloud computing was cheaper, faster, and easier to adopt. Enterprises moved workloads from on-premises infrastructure to public cloud services, relying on major cloud providers to handle scalability, maintenance, and security.&lt;/p&gt;

&lt;p&gt;In 2026, that assumption is breaking, and cracks are showing up in legal reviews, financial projects, and SLA negotiations. Enterprises are facing an increasing pressure for &lt;a href="https://securityboulevard.com/2025/12/the-global-data-residency-crisis-how-enterprises-can-navigate-geolocation-storage-and-privacy-compliance-without-sacrificing-performance/" rel="noopener noreferrer"&gt;data residency regulations&lt;/a&gt;, stricter enforcement, and scrutiny around cloud security models. Compliance constraints, data security requirements, cost predictability, and latency are forcing teams to reconsider on-premises solutions, private cloud computing, and hybrid cloud infrastructure.&lt;/p&gt;

&lt;p&gt;At the same time, AI is moving closer to where data is generated. Manufacturing sites, retail stores, and healthcare environments increasingly require offline capability and sub-100ms latency. That shift helps explain why &lt;a href="https://blogs.oracle.com/database/ga-of-oracle-ai-database-26ai-for-linux-x86-64-on-premises-platforms" rel="noopener noreferrer"&gt;Oracle released AI Database 26ai&lt;/a&gt; for on-premises deployment and why &lt;a href="https://docs.cloud.google.com/distributed-cloud/gemini-on-gdcc/latest/docs/overview" rel="noopener noreferrer"&gt;Google&lt;/a&gt; is pushing Gemini onto Distributed Cloud for air-gapped environments. This shift signals that large-scale enterprise AI no longer fits neatly in cloud environments.&lt;/p&gt;

&lt;p&gt;In this article, we’ll examine why on-premises infrastructure is resurging, what trade-offs you need to know, and how to make defensible deployment decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Driving The On-premises Resurgence
&lt;/h2&gt;

&lt;p&gt;The renewed interest in on-premises infrastructure is not about going back to old systems. It is a response to clear changes in how AI systems are being built and used in 2025 and 2026. For many enterprises, cloud-only vector databases no longer fit their compliance, cost, and reliability needs.&lt;/p&gt;

&lt;p&gt;A lot of factors drive this current on-premises resurgence, but in this article we will consider four key causes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Large vendors now support on-premises AI
&lt;/h3&gt;

&lt;p&gt;On-premises AI is no longer treated as an edge case by major vendors. Oracle’s release of AI Database 26ai and Google’s decision to run Gemini on Distributed Cloud show a clear shift in how enterprise AI is being packaged and delivered.&lt;/p&gt;

&lt;p&gt;These products are built for large enterprises, not early-stage experiments or research projects. That distinction matters. Large vendors do not invest in complex on-premises AI platforms unless there is strong and growing customer demand. These announcements confirm that many enterprises want to run AI systems inside their own environments, close to their data, and under their full operational control. Why is this?&lt;/p&gt;

&lt;h3&gt;
  
  
  Regulatory pressure is now a real blocker
&lt;/h3&gt;

&lt;p&gt;Teams used to plan for regulatory risk as a future possibility. Now it’s a day-to-day reality. GDPR enforcement reached record levels in 2025, with insufficient legal basis for data processing driving the largest penalties. That year alone, regulators issued &lt;a href="https://www.enforcementtracker.com/?insights" rel="noopener noreferrer"&gt;nearly 2,700 fines&lt;/a&gt; totaling billions of euros.&lt;/p&gt;

&lt;p&gt;From a data security perspective, GDPR enforcement has fundamentally changed how enterprises evaluate cloud services. While cloud service providers offer compliance tooling, legal teams are increasingly wary of relying on third-party providers for sensitive data storage and processing.&lt;/p&gt;

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

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

&lt;p&gt;HIPAA adds another layer of complexity. For example, in Florida, physicians must maintain medical records for five years after the last patient contact, whereas hospitals must maintain them for seven years under &lt;a href="https://www.hipaajournal.com/hipaa-retention-requirements/" rel="noopener noreferrer"&gt;state record-retention requirements&lt;/a&gt;. This makes repeated data movement risky and expensive. Financial services and government contractors face similar data sovereignty requirements that limit where data can be stored and processed. In these situations, cloud deployments add legal review, audit work, and ongoing risk. Keeping data on-premises is often the most straightforward way to meet these obligations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge AI requires local and offline operation
&lt;/h3&gt;

&lt;p&gt;AI workloads are increasingly deployed close to where data is created. Manufacturing facilities may operate in air-gapped environments or remote locations with limited connectivity. Retail systems must continue working during network outages. Healthcare applications often require very low latency for real-time decision support.&lt;/p&gt;

&lt;p&gt;In these environments, relying on a remote cloud service introduces risk. Network delays and outages directly affect system reliability. On-premises and edge deployments allow vector search and inference to run locally, without depending on constant network access. For many use cases, this local execution is not an optimization but a requirement.&lt;/p&gt;

&lt;p&gt;Together, these shifts explain why on-premises vector databases are gaining traction again. The change is driven by the practical realities of deploying production AI systems under real regulatory, cost, and reliability constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compliance Calculus
&lt;/h2&gt;

&lt;p&gt;For many enterprises, compliance is the deciding factor in the on-premises versus cloud debate. While cloud providers offer compliance certifications, the real challenge is not whether a platform can be compliant in theory, but whether it can withstand legal review, audits, and long-term operational scrutiny in practice. Once vector databases move into production and begin storing sensitive or regulated data, these questions become unavoidable.&lt;/p&gt;

&lt;h3&gt;
  
  
  GDPR and the limits of cross-border transfers
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://www.pinsentmasons.com/out-law/guides/international-transfers-schrems-ii-gdpr" rel="noopener noreferrer"&gt;Schrems II ruling&lt;/a&gt; changed how European data can be processed outside the EU. Privacy Shield was invalidated, leaving Standard Contractual Clauses as the primary legal mechanism for cross-border data transfers. In highly regulated industries such as financial services and healthcare, many legal teams consider SCCs insufficient due to enforcement uncertainty and ongoing legal challenges.&lt;/p&gt;

&lt;p&gt;For vector databases, this matters because embeddings often contain derived personal data. Even if raw records are masked or tokenized, embeddings can still be considered personal data under GDPR. If data must remain within the EEA, or within a specific country, cloud deployments that rely on global infrastructure introduce legal risk. In these cases, on-premises or in-region deployment becomes a requirement rather than a preference.&lt;/p&gt;

&lt;h3&gt;
  
  
  HIPAA retention and the real cost of data movement
&lt;/h3&gt;

&lt;p&gt;HIPAA does not explicitly require data to stay on-premises, but it does require long retention periods and strict access controls. When vector embeddings are built on top of this data, they inherit the same retention requirements. &lt;a href="https://www.actian.com/blog/data-governance/hipaa-data-governance/" rel="noopener noreferrer"&gt;HIPAA data governance&lt;/a&gt; must be enforced when considering on-premises or cloud vector databases.&lt;/p&gt;

&lt;p&gt;The cost impact becomes clear when egress fees are included. Consider a system storing 100 TB of embeddings in a cloud environment. At a common egress rate of $0.09 per GB, moving that data out of the cloud over a seven-year retention period results in:&lt;/p&gt;

&lt;p&gt;100 TB × $0.09 per GB × 84 months = over $750,000 in egress costs alone&lt;/p&gt;

&lt;p&gt;This does not include compute, storage, or indexing costs. With this in mind, will &lt;a href="https://www.actian.com/blog/cloud-data-warehouse/will-cloud-data-warehouses-really-help-you-cut-costs/" rel="noopener noreferrer"&gt;cloud data warehouses&lt;/a&gt; really help you cut costs?&lt;/p&gt;

&lt;h3&gt;
  
  
  Financial services and data sovereignty rules
&lt;/h3&gt;

&lt;p&gt;Financial institutions face additional constraints beyond GDPR. Regulations such as &lt;a href="https://www.ftc.gov/business-guidance/privacy-security/gramm-leach-bliley-act" rel="noopener noreferrer"&gt;GLBA&lt;/a&gt;, &lt;a href="https://www.apra.gov.au/" rel="noopener noreferrer"&gt;APRA&lt;/a&gt;, and regional data sovereignty mandates often require strict control over where customer data is stored and processed. Regulators may demand clear evidence of geographic boundaries, access controls, and auditability.&lt;/p&gt;

&lt;p&gt;Cloud services can meet some of these requirements, but they often introduce complex configurations, contractual dependencies, and ongoing compliance reviews. For many banks and insurers, on-premises deployment simplifies audits by keeping data within controlled infrastructure that regulators already understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Government and public sector constraints
&lt;/h3&gt;

&lt;p&gt;Government contracts introduce some of the strictest infrastructure requirements. Standards such as &lt;a href="https://www.fedramp.gov/" rel="noopener noreferrer"&gt;FedRAMP&lt;/a&gt; often mandate US-only infrastructure, restricted access, and tightly controlled environments.&lt;/p&gt;

&lt;p&gt;In these cases, public cloud services are frequently disallowed or require extensive approvals. On-premises deployment is often the only viable option for running vector databases in support of government workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  When compliance makes cloud untenable
&lt;/h3&gt;

&lt;p&gt;If legal teams flag cross-border data transfers as unacceptable, cloud deployments quickly become impractical. Once data residency is mandatory, on-premises deployment is no longer a trade-off decision. It is a compliance requirement.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Cost Breakdown Analysis
&lt;/h2&gt;

&lt;p&gt;Cost is often the reason teams revisit the on-premises versus cloud decision. To make a defensible decision, teams need to understand where costs diverge and when self-hosting becomes economically rational.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where self-hosting breaks even
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://openmetal.io/resources/blog/when-self-hosting-vector-databases-becomes-cheaper-than-saas/" rel="noopener noreferrer"&gt;Research from OpenMetal&lt;/a&gt; shows a consistent breakeven point for Pinecone vector databases at scale. Once workloads reach roughly 80 to 100 million queries per month, self-hosted deployments tend to be cheaper than managed cloud services. Below this range, cloud pricing is usually competitive. Above it, usage-based billing begins to dominate total cost.&lt;/p&gt;

&lt;p&gt;This threshold matters because many enterprise RAG systems cross it quickly. Customer support, document search, fraud detection, and recommendation systems often serve tens or hundreds of millions of queries each month once deployed across business units or regions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hidden cost in cloud pricing
&lt;/h3&gt;

&lt;p&gt;Cloud pricing is rarely just a per-query fee. Vector databases introduce several cost drivers that are easy to overlook during planning.&lt;/p&gt;

&lt;p&gt;Egress fees are a major factor. Most cloud providers charge around $0.09 per GB for data leaving their network. Moving embeddings between regions, exporting data for analytics, or migrating to another system all incur these fees. Over time, they become a meaningful portion of total spend.&lt;/p&gt;

&lt;p&gt;Finally, vector search does not scale linearly. As vector counts grow and dimensionality increases, query costs rise faster than expected. What looks affordable at 10 million vectors can become expensive at 500 million, even if query volume grows steadily.&lt;/p&gt;

&lt;h3&gt;
  
  
  On-premises costs are fixed and predictable
&lt;/h3&gt;

&lt;p&gt;On-premises deployments have real costs, but they behave differently. Hardware is typically amortized over three to five years. Staffing requirements are stable once the system is running. Facilities and power costs are known in advance.&lt;/p&gt;

&lt;p&gt;The key difference is predictability. Costs do not spike because of usage patterns or data movement. Once the system is sized correctly, monthly spend remains largely flat, even as query volume increases.&lt;/p&gt;

&lt;h3&gt;
  
  
  A real world example
&lt;/h3&gt;

&lt;p&gt;Consider a production e-commerce application with the following scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;500M vectors&lt;/li&gt;
&lt;li&gt;200M queries every month&lt;/li&gt;
&lt;li&gt;1024 vector dimensions&lt;/li&gt;
&lt;li&gt;6M writes monthly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this scale, a typical managed Pinecone vector database costs around $8,500 per month once compute, storage, and rebuild overhead are included.&lt;/p&gt;

&lt;h3&gt;
  
  
  Estimated monthly cost
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Total Estimated Cost:&lt;/strong&gt;  &lt;strong&gt;$8,454 / month&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Storage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usage:&lt;/strong&gt; 845 GB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt;  &lt;strong&gt;$279&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Query Costs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Configuration:&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;24 &lt;strong&gt;b1 nodes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;4 shards × 6 replicas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assumption:&lt;/strong&gt; 1% filter selectivity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimated Cost:&lt;/strong&gt;   &lt;strong&gt;$8,074&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Note: Actual query cost may vary. Benchmark your workload on DRN for more accurate estimates.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Write Costs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write Volume:&lt;/strong&gt; 30 million Write Units (WU)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assumption:&lt;/strong&gt; Each write request consumes &lt;strong&gt;≥ 5 WU&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt;  &lt;strong&gt;$101&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;An equivalent on-premises deployment might cost approximately half of that after hardware amortization, assuming an 18-month payback period and one to two engineers supporting the system. After that payback period, costs drop further while capacity remains available.&lt;/p&gt;

&lt;p&gt;A study by &lt;a href="https://www.enterprisestorageforum.com/cloud/on-premise-vs-cloud-storage/" rel="noopener noreferrer"&gt;Enterprise Storage Forum&lt;/a&gt; shows the cost projection of on-premises and cloud workloads.&lt;/p&gt;

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

&lt;p&gt;Cost alone does not decide every deployment, but once vector workloads reach scale, the economics become difficult to ignore. Understanding where your system sits on this curve is essential before locking in a long-term vector database strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Latency And Connectivity Matter
&lt;/h2&gt;

&lt;p&gt;Latency and connectivity are often treated as secondary concerns in architecture decisions. For many AI workloads, they are decisive. Once vector databases support real-time systems, network round-trips and internet dependency can make cloud deployments impractical or unsafe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-time response requirements
&lt;/h3&gt;

&lt;p&gt;Some applications have strict response time limits. In healthcare, clinical decision support and diagnostic systems often require responses in under 50 milliseconds. This budget includes data retrieval, vector search, and model inference. Similarly, banks and financial institutions often require very low latency for maximum user experience.&lt;/p&gt;

&lt;p&gt;Public cloud deployments add unavoidable network latency. Even within the same region, round-trip latency typically adds 20 to 80 milliseconds before any compute work begins. For applications with tight latency targets, this overhead alone can exceed the total allowed response time. On-premises deployments remove that network hop, allowing systems to meet real-time requirements consistently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Systems that must work offline
&lt;/h3&gt;

&lt;p&gt;Many environments cannot rely on constant connectivity. Retail point-of-sale systems must continue operating during network outages. Manufacturing facilities are often located in remote areas with unstable connections. Military and maritime deployments may operate in fully disconnected or classified environments.&lt;/p&gt;

&lt;p&gt;In these scenarios, a cloud dependency is a single point of failure. If the network goes down, the AI system stops working. On-premises and edge deployments allow vector search and inference to run locally, ensuring the system continues to function even when external connectivity is unavailable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost of downtime
&lt;/h3&gt;

&lt;p&gt;It is no news that there has been an increase in downtime from cloud providers. On November 18, 2025, &lt;a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noopener noreferrer"&gt;Cloudflare outage&lt;/a&gt; disrupted large portions of the internet, causing &lt;a href="https://www.thenational.scot/news/national/uk-today/25630748.full-list-sites-affected-cloudflare-outage-like-x/" rel="noopener noreferrer"&gt;downtime across major platforms&lt;/a&gt;including X, Amazon Web Services, Spotify, and so on. The impact of connectivity failures is not theoretical. In manufacturing, average downtime costs are &lt;a href="https://www.twi-institute.com/manufacturing-downtime/" rel="noopener noreferrer"&gt;estimated at $260,000 per hour&lt;/a&gt;. When AI systems support quality control, predictive maintenance, or process automation, any outage directly affects production.&lt;/p&gt;

&lt;p&gt;A cloud-only architecture introduces risk that is hard to justify in these environments. Even short network disruptions can lead to significant financial loss. On-premises deployments reduce this risk by removing external dependencies from critical execution paths.&lt;/p&gt;

&lt;p&gt;For workloads with strict latency targets or limited connectivity, the choice is often clear. Cloud-based vector databases may work during development, but they fail to meet operational requirements in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Operational Complexity Question
&lt;/h2&gt;

&lt;p&gt;The strongest argument for cloud vector databases is operational simplicity. Managed services remove the need to provision hardware, manage clusters, apply patches, or handle failures. For small teams or early-stage projects, this advantage is real and often decisive. Cloud deployments allow engineers to focus on application logic rather than infrastructure.&lt;/p&gt;

&lt;p&gt;It is also important to recognize that modern on-premises deployments look very different from those of a decade ago. This is not the world of manual server provisioning and fragile scripts. Kubernetes, infrastructure-as-code, and automated deployment pipelines have reduced operational overhead significantly. Rolling upgrades, automated scaling, and monitoring are now standard practices in on-premises environments as well as in the cloud.&lt;/p&gt;

&lt;p&gt;Many enterprises adopt hybrid approaches to balance speed and control. Development and experimentation happen in the cloud, where teams can move quickly and iterate. Production systems run on-premises, where costs are predictable and compliance is easier to enforce. This pattern allows teams to get the best of both models without committing fully to either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Framework: Eight Questions
&lt;/h2&gt;

&lt;p&gt;The fastest way to make a defensible deployment decision is to walk through a small set of yes or no questions with engineering, legal, finance, and operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Does your data require geographic restrictions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Regulations such as GDPR, HIPAA, and financial services rules may limit where data can be stored or processed.&lt;/p&gt;

&lt;p&gt;If yes, on-premises should be strongly considered because it provides full control over data location. If no, cloud deployment remains viable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Do you have predictable, high-volume query patterns?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cloud vector database costs scale with usage. A simple check is monthly queries multiplied by the unit cost.&lt;/p&gt;

&lt;p&gt;If usage exceeds roughly 80 to 100 million queries per month, on-premises is often cheaper. Below that range, cloud pricing is usually more economical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Do you need offline capability?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some systems must continue working without network access, such as in manufacturing, retail, or edge environments.&lt;/p&gt;

&lt;p&gt;If yes, on-premises is required. If no, cloud remains an option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Can you tolerate additional latency?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cloud deployments add network latency, often 50 to 100 milliseconds.&lt;/p&gt;

&lt;p&gt;If your application cannot tolerate this, on-premises deployment is necessary. If it can, cloud performance may be acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Do you have existing infrastructure teams?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Operational capacity matters.&lt;/p&gt;

&lt;p&gt;If you already run on-premises systems, the added burden is limited. If not, cloud-managed services provide a clear operational advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Is cost predictability important?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usage-based billing introduces cost variability.&lt;/p&gt;

&lt;p&gt;If predictable costs matter, on-premises provides stability. If flexibility matters more, cloud pricing may be a better fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Are you extending existing IT infrastructure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deployment context affects the decision.&lt;/p&gt;

&lt;p&gt;If you are extending existing systems, on-premises leverages current investments. If you are building something new, cloud may be faster to deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. How large is your data footprint?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Data volume and access frequency influence long-term cost.&lt;/p&gt;

&lt;p&gt;If you manage more than 10 TB with frequent access, on-premises becomes attractive. If your data is smaller, cloud is often sufficient.&lt;/p&gt;

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

&lt;p&gt;When several answers point in the same direction, the decision becomes easy to explain and defend across engineering, legal, finance, and operations teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Cloud Makes Sense
&lt;/h2&gt;

&lt;p&gt;On-premises deployment is not always the right answer. In many situations, cloud-based vector databases remain the better choice. Being clear about these cases helps avoid over-engineering.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unpredictable scaling:&lt;/strong&gt; Startups and new products often face uncertain growth. Cloud platforms allow rapid scaling without long-term infrastructure commitments, which reduces risk when demand is unclear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small data volumes:&lt;/strong&gt; When total data is under 10 TB and query volume stays below about 50 million queries per month, cloud pricing usually works well and is simpler than self-hosting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rapid experimentation:&lt;/strong&gt; Proof-of-concepts, research projects, and early prototypes benefit from fast setup and easy teardown. Cloud services support quick iteration with minimal operational effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No compliance constraints:&lt;/strong&gt; If data residency, sovereignty, and regulatory requirements are not an issue, cloud deployment avoids legal complexity and speeds up delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited infrastructure expertise:&lt;/strong&gt; Teams focused on application logic rather than operations can rely on managed services instead of maintaining databases, clusters, and hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these cases, cloud is the most effective and practical option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid Deployment Strategies
&lt;/h2&gt;

&lt;p&gt;Hybrid deployments act as the middle ground for enterprises that need both speed and control. Rather than treating cloud and on-premises as mutually exclusive, teams place each part of the system where it performs best.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud for iteration, on-prem for scale
&lt;/h3&gt;

&lt;p&gt;A common pattern is to develop and test in the cloud, where managed services and elastic infrastructure enable rapid iteration. Once models, indexes, or pipelines are stable, they are promoted into on-premises production environments to meet compliance, latency, and operational requirements. This preserves developer velocity without compromising production guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data segregation by risk and regulation
&lt;/h3&gt;

&lt;p&gt;Hybrid architectures also allow organizations to separate workloads by risk profile. Sensitive or regulated data stays on-premises, while analytics, training, or search over derived data runs in the cloud. The same logic applies regionally: EU data may remain on-premises or in sovereign environments, while US workloads run in public cloud regions, avoiding global systems being constrained by the strictest jurisdiction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost and migration flexibility
&lt;/h3&gt;

&lt;p&gt;Cost optimization is another driver. Frequently accessed vectors or low-latency services can be cheaper and more predictable on-premises, while cold storage and bursty workloads benefit from cloud pricing. Many teams start cloud-first, then selectively move components on-premises as scale or compliance pressures grow. Hybrid makes this a controlled evolution rather than a disruptive rewrite.&lt;/p&gt;

&lt;p&gt;Industry research shows this is a stable operating model. &lt;a href="https://cloud.google.com/distributed-cloud" rel="noopener noreferrer"&gt;Google Distributed Cloud&lt;/a&gt; and similar platforms explicitly frame hybrid as a long-term strategy, recognizing that modern systems are designed to span environments, not collapse them into one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actian’s Approach To On-premises Vector Databases
&lt;/h2&gt;

&lt;p&gt;For teams that conclude that on-premises is the right deployment model, the next question is: which platform can actually meet these requirements? &lt;a href="https://www.actian.com/" rel="noopener noreferrer"&gt;Actian’s approach&lt;/a&gt; is built specifically for this audience, without assuming the cloud is the default or the end state.&lt;/p&gt;

&lt;p&gt;Actian delivers an &lt;a href="https://www.actian.com/blog/databases/introducing-actian-vector-7-0/" rel="noopener noreferrer"&gt;enterprise-grade vector database&lt;/a&gt; that runs fully in your own data center or controlled environments. You retain full control over data placement, networking, and operations. There is no forced dependency on external cloud services, which simplifies audits and long-term system design.&lt;/p&gt;

&lt;p&gt;Compliance requirements are treated as baseline constraints. By keeping data local and eliminating egress paths, Actian aligns with GDPR, HIPAA, FedRAMP, and similar regulatory frameworks. This reduces the need for compensating controls or complex legal workarounds.&lt;/p&gt;

&lt;p&gt;Cost behavior is also predictable. Actian avoids usage-based pricing models that scale with queries or vector counts. This makes budgeting simpler and removes surprises as workloads grow.&lt;/p&gt;

&lt;p&gt;Edge support is also taken into consideration. Actian’s architecture supports offline operation and local inference, making it suitable for manufacturing sites, retail locations, and other environments where connectivity is limited or unreliable. The system is designed to keep working even when the network does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Choosing between cloud and on-premises for vector databases is about understanding your priorities. Cloud works well for small workloads, rapid experimentation, and teams without deep infrastructure expertise. On-premises makes sense when compliance, latency, cost predictability, or scale are critical.&lt;/p&gt;

&lt;p&gt;Many enterprises find a hybrid approach is the best balance, combining cloud flexibility with on-premises control. The key is making intentional decisions based on your data, workloads, and regulatory needs rather than following trends.&lt;/p&gt;

&lt;p&gt;Actian empowers enterprises to confidently manage and govern data at scale. Organizations trust Actian data management and data intelligence solutions to streamline complex data environments and accelerate the delivery of AI-ready data. As the data and AI division of &lt;a href="https://www.hcl-software.com/" rel="noopener noreferrer"&gt;HCLSoftware&lt;/a&gt;, Actian helps enterprises manage and govern data at scale across on-premises, cloud, and hybrid environments. Learn more about &lt;a href="https://www.actian.com/databases/vector-ai-db/#waitlist" rel="noopener noreferrer"&gt;Actian&lt;/a&gt; and how it fits into your on-premises AI strategy.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>database</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>What if ML pipelines had a lock file?</title>
      <dc:creator>Offisong Emmanuel</dc:creator>
      <pubDate>Wed, 11 Feb 2026 16:16:24 +0000</pubDate>
      <link>https://dev.to/hackmamba/what-if-ml-pipelines-had-a-lock-file-24f</link>
      <guid>https://dev.to/hackmamba/what-if-ml-pipelines-had-a-lock-file-24f</guid>
      <description>&lt;p&gt;I spent two hours last month staring at identical Git commits trying to figure out why my model retrain had different results.&lt;/p&gt;

&lt;p&gt;The code was the same. The hyperparameters were the same. I was even running on the same machine. But the validation metrics had shifted by 12%, and I couldn't explain why. I checked everything twice: my random seeds were fixed, my dependencies were pinned, my Docker image hadn't changed. Then I looked at the data.&lt;/p&gt;

&lt;p&gt;Someone had added a column to an upstream table and backfilled it. Nothing broke. The pipeline kept running. Training succeeded. But the feature distribution had shifted, and the model had learned from data that no one realized was different.&lt;/p&gt;

&lt;p&gt;That experience changed how I think about ML pipelines. We can lock dependencies. We can lock infrastructure. But the computation itself has no identity. Pipelines are still scripts that read mutable data, assume schemas that drift, and depend on execution details that change quietly. &lt;/p&gt;

&lt;p&gt;In this article, we’ll walk through why that makes ML pipelines hard to reproduce, what a pipeline lock file actually needs to capture, and how treating computation as an artifact changes how we debug, audit, and build models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ML pipelines are hard to reproduce
&lt;/h2&gt;

&lt;p&gt;When an ML pipeline fails to reproduce, the code is rarely the problem. Most teams already version their training scripts, feature logic, and model code using &lt;a href="https://git-scm.com/" rel="noopener noreferrer"&gt;Git&lt;/a&gt;. The issue is that the meaning of that code depends on far more than what lives in the repository. &lt;/p&gt;

&lt;p&gt;Consider a fraud detection pipeline use-case. The code reads transaction data, joins it with user profiles, applies feature transformations, and trains a model. The Python script and SQL queries are tracked in Git. The model architecture is documented. Everything looks reproducible. &lt;/p&gt;

&lt;p&gt;After a while, fraud detection accuracy drops in production, and you are tasked to recreate the training run for an audit, but you can't. The code runs, but the model comes out different. Something changed, but what?&lt;/p&gt;

&lt;p&gt;The problem is that ML pipelines don't just depend on code. They depend on data, schemas, and execution details that live outside the repository and change without anyone noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data&lt;/strong&gt;&lt;br&gt;
Pipelines usually read from tables that change over time. Most of these tables are stored in a data warehouse like &lt;a href="https://aws.amazon.com/redshift/" rel="noopener noreferrer"&gt;Amazon Redshift&lt;/a&gt; or &lt;a href="https://cloud.google.com/bigquery" rel="noopener noreferrer"&gt;Google&lt;/a&gt; &lt;a href="https://cloud.google.com/bigquery" rel="noopener noreferrer"&gt;BigQuery&lt;/a&gt;. Rows are added or removed. Backfills happen. A column gets renamed or its meaning changes. Even when teams snapshot data, those snapshots are often implicit, not recorded as part of the pipeline run itself. &lt;/p&gt;

&lt;p&gt;In this fraud pipeline, training data comes from a warehouse table like &lt;code&gt;transactions&lt;/code&gt;. Between the original training run and the reproduction attempt, the data team backfilled several months of historical records to fix a reporting bug. The pipeline query didn’t change:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM transactions WHERE date &amp;gt;= '2025-01-01'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;But the rows returned did.&lt;/p&gt;

&lt;p&gt;The original model was trained on one set of data (transaction amounts, merchant categories, and user behavior), while the reproduced run was trained on a different set. Even though both runs used the same code, neither recorded which specific data version was used.&lt;/p&gt;

&lt;p&gt;From the outside, it looks like “the same pipeline.” In reality, two different datasets flowed through it.&lt;/p&gt;

&lt;p&gt;The problem is even worse with derived tables. If the fraud model depends on a shared feature table maintained by another team, and that team fixes a bug in their aggregation logic and recomputes the table, our pipeline can keep running and silently consume the updated features. There is no error or warning, just different inputs flowing into the same code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schemas&lt;/strong&gt;&lt;br&gt;
Schemas add another layer of fragility. Many pipelines assume schemas rather than enforce them.&lt;br&gt;
During the fraud detection data backfill, the schema changed, too. A new column, &lt;code&gt;merchant_risk_score&lt;/code&gt;, was added to the transactions table. It was nullable at first because historical data didn’t have values for it yet.&lt;/p&gt;

&lt;p&gt;The feature pipeline didn’t break. It simply treated missing values as zero during normalization. That meant older transactions effectively had &lt;em&gt;no merchant risk&lt;/em&gt;, while newer ones suddenly did. The feature still existed. The code still ran. But the meaning of the feature changed.&lt;/p&gt;

&lt;p&gt;As a result, the model learned two different behaviors depending on when a transaction occurred. Recent data emphasized merchant risk. Older data didn’t. Overall metrics looked fine during training, but once deployed, the model began misclassifying edge cases in production.&lt;/p&gt;

&lt;p&gt;When accuracy dropped, the team assumed normal data drift and retrained. The retrain succeeded, but the new model still didn’t match the original. The schema change had rewritten the semantics of the features, and nothing in the pipeline recorded that shift or made it visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependencies and execution details&lt;/strong&gt;&lt;br&gt;
Dependencies and execution details add another layer of instability. A query planner may choose a different plan. A caching layer may reuse an old result. A User Defined Function (UDF) can change behavior because one of its dependencies was updated. None of this shows up in git, and very little of it is visible in logs.&lt;/p&gt;

&lt;p&gt;Caching sometimes alters your model performance. They speed things up, which is good. But they also introduce a hidden state that can change results between runs. For example, your pipeline caches a feature table. Someone updates the upstream logic. Your cache is now stale, but nothing tells you that. You're training on a mix of old features and new data.&lt;/p&gt;

&lt;p&gt;Even the runtime version matters. The original model artifact had been serialized with Python 3.9, but the reproduction ran under Python 3.11. The model loaded successfully, but downstream behavior wasn’t identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;&lt;br&gt;
The pipeline was reproducible in theory, but not in practice. The same code ran. A different computation happened.&lt;/p&gt;

&lt;p&gt;There was no single artifact to inspect. No receipt that captured the data that was read, the schemas that were assumed, the UDF logic that executed, or the cache state that influenced the result. The team spent weeks reconstructing the run from logs, guesses, and tribal knowledge.&lt;/p&gt;

&lt;p&gt;This is the gap lock files solved for software dependencies. And it’s the same gap ML pipelines still have today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why existing tools don’t fix this
&lt;/h2&gt;

&lt;p&gt;At this point, most teams reach for familiar fixes.&lt;/p&gt;

&lt;p&gt;They add more logging. They version datasets manually. They pin library versions. They introduce orchestrators, lineage tools, and experiment trackers. Each tool helps in isolation, but none of them answer the one question that matters during an incident or an audit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually ran?&lt;/strong&gt;&lt;br&gt;
Logs tell you that a job executed, not which data it read. Git tells you what the code looked like, not how it resolved at runtime. Lineage graphs show connections, but not the concrete inputs, schemas, or cached state used in a specific run. Experiment tracking stores metrics and artifacts, but not the computation that produced them. So when something goes wrong, teams are left reconstructing history from fragments and guesswork.&lt;/p&gt;

&lt;p&gt;The deeper issue is that ML pipelines don’t produce a durable artifact of the computation itself. The code is versioned, but the resolved execution is not. Data is mutable. Schemas drift. Execution details change. And none of that has a stable identity you can point to later.&lt;/p&gt;

&lt;p&gt;Software engineering solved this problem years ago. We didn’t fix reproducibility by writing better README files or adding more logs. We fixed it by introducing lock files. Lock files are machine-readable artifacts that capture the fully resolved state of a system at execution time, representing the actual thing that ran rather than configuration.&lt;/p&gt;

&lt;p&gt;The missing piece in ML is the same idea, applied to computation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an ML pipeline lock file actually is
&lt;/h2&gt;

&lt;p&gt;An ML pipeline lock file is not a configuration file. It is not another place to declare what you want to run. It is a record of what actually ran.&lt;/p&gt;

&lt;p&gt;In software, a lock file answers a simple question: What was installed? Not which dependencies were requested, but which ones were resolved, down to exact versions and hashes. An ML pipeline lock file needs to answer the same kind of question, but for computation. What computation is this?&lt;/p&gt;

&lt;p&gt;That requires three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An explicit computation graph&lt;/li&gt;
&lt;li&gt;Content identities&lt;/li&gt;
&lt;li&gt;Roundtrippability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;An explicit computation graph&lt;/strong&gt;&lt;br&gt;
The lock file must capture the computation as a concrete object. Not a Python script that does things, but the actual reads, transformations, joins, aggregations, UDFs, and caches that make up the pipeline. &lt;/p&gt;

&lt;p&gt;For example, when you look at &lt;code&gt;package-lock.json&lt;/code&gt;, you don't see installation scripts. You see the resolved dependency tree. Each package, each version. The lock file for an ML pipeline needs the same clarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content identities&lt;/strong&gt;&lt;br&gt;
Every piece of the computation needs an identity based on its content. The inputs you read. The UDFs you execute. The dependencies you use. The cached artifacts you produce. Same inputs should mean the same identity and different inputs should mean different identities.&lt;/p&gt;

&lt;p&gt;If two runs have the same content identities for their inputs, UDFs, and dependencies, they're running the same computation. If any of those identities differ, something changed. You don't have to guess. You can check the hashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roundtrippability&lt;/strong&gt;&lt;br&gt;
One of the core features of an ML lock file is roundtrippability. A real pipeline lock file must be runnable on its own. Given the lock file and its associated artifacts, you should be able to rerun the pipeline without relying on a particular machine, environment, or set of hidden caches.&lt;/p&gt;

&lt;p&gt;If your lock files have these features, you can diff computations the way you diff lock files. You can verify that a rerun is actually running the same thing. You can cache based on content, not guesses. You can bisect regressions by comparing hashes instead of reading through logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git vs. Manifests
&lt;/h2&gt;

&lt;p&gt;A useful way to understand the value of manifests is to compare what traditional version control captures with what a build manifest records. Git excels at tracking &lt;em&gt;how&lt;/em&gt; a pipeline is written, but it stops short of describing the fully resolved computation that actually executed. The manifest (&lt;code&gt;expr.yaml&lt;/code&gt;) fills in that missing layer by freezing the execution-time reality of the pipeline.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Code (git)&lt;/th&gt;
&lt;th&gt;Manifest (expr.yaml)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline definition&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolved inputs at execution time&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema contracts&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UDF and UDXF content hashes&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached artifacts&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What actually ran&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Git is excellent at tracking the source code that defines a pipeline. The manifest goes further by recording the resolved state of that pipeline at execution time. &lt;/p&gt;

&lt;h2&gt;
  
  
  Create an ML lock file using Xorq
&lt;/h2&gt;

&lt;p&gt;Once you understand what a pipeline lock file is and why it matters, the next step is seeing it in action. &lt;a href="https://github.com/xorq-labs/xorq" rel="noopener noreferrer"&gt;Xorq&lt;/a&gt; makes it straightforward to turn a declarative pipeline into a reproducible, versioned artifact with a lock file.&lt;/p&gt;

&lt;p&gt;To get started, install Xorq using pip or uv:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install "xorq[examples]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;or&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv add "xorq[examples]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Next, download the &lt;a href="https://www.kaggle.com/datasets/umitka/synthetic-financial-fraud-dataset" rel="noopener noreferrer"&gt;financial fraud dataset&lt;/a&gt; from Kaggle and place the CSV file in your working directory. This example uses a simplified fraud detection pipeline, but the structure mirrors what you would build in a real production system.&lt;/p&gt;

&lt;p&gt;Create a file &lt;code&gt;main.py&lt;/code&gt; with the following content:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import xorq.api as xo
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline
from xorq.caching import ParquetCache
from xorq.config import options
import os
# specifies cache directory as current directory/cache
options.cache.default_relative_path=f"{os.getcwd()}/cache"
con = xo.connect()
cache = ParquetCache.from_kwargs()
# 1. Load the dataset
data = xo.read_csv('synthetic_fraud_dataset.csv')
# 2. Train / test split
train, test = xo.train_test_splits(data, test_sizes=0.2)
sk_pipeline = Pipeline([
    ("model", RandomForestClassifier(
        n_estimators=200,
        max_depth=10,
        random_state=42
    ))
])
# 3. Define the model
model = xo.Pipeline.from_instance(sk_pipeline)
# 4. Fit the model
fitted = model.fit(
    train,
    features=[
        'amount',
        'hour',
        'device_risk_score',
        'ip_risk_score'
    ],
    target='is_fraud'
)
# 5. Generate predictions (deferred execution)
predictions = fitted.predict(test).cache(cache=cache)
# 6. Execute the computation
print(predictions.execute())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A few important things are happening here. The entire pipeline is defined declaratively, with each step clearly described: data ingestion, train–test splitting, model configuration, and a cached prediction stage. Nothing runs until execution is requested. When it does run, Xorq has enough information to capture the full computation as an explicit graph.&lt;/p&gt;

&lt;p&gt;At this point, you have a working ML pipeline. In the next step, instead of just running it, we will build it. That build step is what produces the lock file: a manifest that records the resolved computation, the data it read, the schemas it assumed, the cached artifacts it created, and the exact logic that ran.&lt;/p&gt;

&lt;p&gt;If your project directory is not already a Git repository, you need to initialize one before building an expression. Xorq records the git state as part of the build metadata, so a repository with at least one commit is required.&lt;/p&gt;

&lt;p&gt;Run the following commands in your project folder:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git init
git add .
git commit -m "initial commit"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Once the repository is initialized, you can build the expression and generate the lock file by running:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xorq build main.py -e predictions 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you are using uv, the equivalent command is:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uv run xorq build main.py -e predictions 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This build step is what turns your pipeline from a runnable script into a versioned artifact, complete with a manifest that records the resolved computation.&lt;/p&gt;

&lt;p&gt;The output of the run is shown in the image below:&lt;/p&gt;

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

&lt;p&gt;After the build completes, you should see two new directories: &lt;code&gt;builds&lt;/code&gt; and &lt;code&gt;cache&lt;/code&gt;. The &lt;code&gt;cache&lt;/code&gt; directory holds cached intermediate results created during execution. The &lt;code&gt;builds&lt;/code&gt; directory contains the build artifacts themselves. Inside &lt;code&gt;builds&lt;/code&gt;, you will find a directory named with a content derived hash, for example &lt;code&gt;78ff43314468&lt;/code&gt;. This directory is the lock file in practice. It is the concrete, portable representation of the pipeline run.&lt;/p&gt;

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

&lt;p&gt;Within that directory, several files are generated automatically, including &lt;code&gt;expr.yaml&lt;/code&gt;, &lt;code&gt;metadata.json&lt;/code&gt;, and &lt;code&gt;profiles.yaml&lt;/code&gt;. The most important of these is &lt;code&gt;expr.yaml&lt;/code&gt;. This file is the receipt for what actually ran. It describes the computation graph, the resolved inputs, the schema contracts, the cached nodes, and the content hashes that give the pipeline its identity.&lt;/p&gt;

&lt;p&gt;Taken together, the build directory is a versioned, cached, and portable artifact. Once it exists, workflows that were previously fragile or manual become straightforward: reproducible runs, diffable computation, bisectable regressions, portable artifacts, and, importantly, composition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The expression file&lt;/strong&gt;&lt;br&gt;
At first glance, &lt;code&gt;expr.yaml&lt;/code&gt; looks intimidating. It contains many components, but its purpose is simple. It describes the computation itself, explicitly and completely.&lt;/p&gt;

&lt;p&gt;Below is an abridged example: &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nodes:
    '@read_4d6c147c9486':
      op: Read
      method_name: read_parquet
      name: ibis_read_csv_nepinfk5dzbxja2bo4kycwisyq
      profile: 846181d9920579c7c1b10dd45b3ab9b2_0
      read_kwargs:
      - - path
        - builds/78ff43314468/database_tables/917eccee9a442913a8c1afca12cf69b0.parquet
      - - table_name
        - ibis_read_csv_nepinfk5dzbxja2bo4kycwisyq
      normalize_method: fvfvfvfvf
      schema_ref: schema_c4a0925bdfca
      snapshot_hash: 4d6c147c9486fe2f5140558ff6860b60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This first node answers a deceptively important question: What data was read? Not “which table name,” and not “which query,” but the &lt;em&gt;exact&lt;/em&gt; data source. The &lt;code&gt;Read&lt;/code&gt; node points to a concrete file, often materialized into the build directory itself. That means the pipeline is tied to the data that was actually used, not whatever that table happens to contain today.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;schema_ref&lt;/code&gt; is part of the plan. If the schema changes, this node no longer matches, and the computation’s identity changes with it.&lt;/p&gt;

&lt;p&gt;Now look at how transformations are represented:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    '@filter_d5f72ffce15d':
      op: Filter
      parent:
        node_ref: '@read_4d6c147c9486'
      predicates:
      - op: LessEqual
        left:
          op: Multiply
          left:
            op: Cast
     predicted:
          op: ExprScalarUDF
          class_name: _predicted_18c1451165c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The code above describes the filter. The predicate itself is part of the graph, not hidden inside a function call or a SQL string. The filter is explicitly connected to its parent node, so there is no ambiguity about ordering or dependencies.&lt;/p&gt;

&lt;p&gt;Every transformation builds on a previous node, forming a complete expression tree:&lt;br&gt;
&lt;strong&gt;Read → Filter → Aggregate → Cache&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Later in the file, you’ll see nodes like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'@cachednode_e7b5fd7cd0a9':
  op: CachedNode
  parent:
    node_ref: '@remotetable_9a92039564d4'
  cache:
    type: ParquetCache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Caching is also part of the computation. Because the cache appears in the graph, it is reproducible and portable. There are no hidden cache keys, no local assumptions, and no silent reuse of stale results. If the upstream logic changes, the cache node’s identity changes too.&lt;/p&gt;

&lt;p&gt;Finally, notice the node names themselves:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@read_4d6c147c9486
@filter_d5f72ffce15d
@cachednode_e7b5fd7cd0a9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;These identifiers are content-derived. They are hashes of the node’s inputs, logic, schema, and configuration. Change anything meaningful, and the identifier changes. That change propagates through the graph.&lt;/p&gt;

&lt;p&gt;This is what makes &lt;code&gt;expr.yaml&lt;/code&gt; a lock file. Instead of saying “run this Python script,” it records what computation resolved, what data it read, what schemas it assumed, and where caching occurred. The hash of the build becomes the identity of the computation itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treating pipelines as building blocks
&lt;/h2&gt;

&lt;p&gt;So far, we’ve looked at how Xorq turns a pipeline into a versioned artifact. The payoff comes because these artifacts are composable. When you build a pipeline with Xorq, the output isn’t just a model or a metric. It’s a versioned computation artifact with a stable hash e.g. &lt;code&gt;xyz123&lt;/code&gt;. That hash represents the fully resolved training run: data, schemas, feature logic, and execution details.&lt;/p&gt;

&lt;p&gt;Because that artifact has an identity, it can be reused. An inference pipeline can explicitly reference the training artifact it depends on. Instead of “load the latest model,” it loads &lt;em&gt;the model produced by build&lt;/em&gt; &lt;code&gt;*xyz123*&lt;/code&gt;, along with the exact feature definitions and schema contracts that training used. If training changes, inference doesn’t silently drift. The composition produces a new hash.&lt;/p&gt;

&lt;p&gt;This also makes deployment seamless. You can easily rollback to previous hashes without guesswork. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is&lt;/strong&gt; &lt;strong&gt;this&lt;/strong&gt; &lt;strong&gt;different from experiment tracking&lt;/strong&gt;&lt;strong&gt;?&lt;/strong&gt;&lt;br&gt;
Tools like MLflow track artifacts. DVC versions data. Both are useful but neither gives you composable, versioned computation graphs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MLflow can tell you which model file was produced, but not the resolved computation that created it.&lt;/li&gt;
&lt;li&gt;DVC can version datasets, but not how those datasets were transformed, joined, cached, and consumed end-to-end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Xorq’s unit of composition is the computation itself. Training pipelines produce artifacts that inference pipelines can depend on directly, without re-encoding assumptions in glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do we gain from this?
&lt;/h2&gt;

&lt;p&gt;The most immediate gain is reproducibility. With a pipeline lock file, rerunning a pipeline means rerunning the same computation, not just the same code. The inputs are fixed, the schemas are known, the logic is explicit, and cached artifacts are part of the record. “Works on my machine” stops being a concern because the computation has a concrete identity.&lt;/p&gt;

&lt;p&gt;You can easily run builds by:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xorq run builds/&amp;lt;build-hash&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Another advantage is portability. This means you can take a build produced on a developer’s laptop and execute it in CI, inside a container, or on a different execution engine with confidence that it will behave the same way.&lt;/p&gt;

&lt;p&gt;Also, when a model regresses, you can diff runs. Two builds produce two manifests. Instead of guessing what changed, you get a semantic diff: data sources, schema changes, UDF content, planner decisions, cached nodes. This turns multi-week investigations into focused comparisons.&lt;/p&gt;

&lt;p&gt;Schema drift becomes visible early. Because schemas are part of the contract, drift shows up at boundaries rather than leaking silently into downstream logic. Pipelines fail fast, in the right place, instead of producing subtly wrong models.&lt;/p&gt;

&lt;p&gt;Finally, there is an organizational gain. When computation is explicit and versioned, teams move faster with less risk. Audits become tractable because training runs are reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing insights
&lt;/h2&gt;

&lt;p&gt;Lock files changed how we think about software. They gave us a stable unit we could diff, ship, and trust. ML pipelines have needed the same thing for a long time, but until now, there has been nothing concrete to lock.&lt;/p&gt;

&lt;p&gt;By giving computation an identity, pipeline manifests turn runs into artifacts. They capture what actually ran, not just what the code described. Once that exists, reproducibility, debugging, audits, and collaboration stop being fragile processes and start becoming mechanical.&lt;/p&gt;

&lt;p&gt;Xorq provides a practical and robust foundation for building reproducible, auditable, and production-grade ML workflows. This makes it easy to generate an ML lock file that captures not just &lt;em&gt;what was written&lt;/em&gt;, but what actually ran, including resolved inputs, content hashes, and cached artifacts. &lt;/p&gt;

&lt;p&gt;For more information about Xorq, head over to their &lt;a href="https://github.com/xorq-labs/xorq" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or their &lt;a href="https://docs.xorq.dev/" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>mlops</category>
      <category>xorq</category>
    </item>
  </channel>
</rss>
