<?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: Sid Probstein</title>
    <description>The latest articles on DEV Community by Sid Probstein (@sidswirl).</description>
    <link>https://dev.to/sidswirl</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%2F1132432%2F063b263f-a2ce-4db8-b6ff-b96cf0655d3c.jpeg</url>
      <title>DEV Community: Sid Probstein</title>
      <link>https://dev.to/sidswirl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sidswirl"/>
    <language>en</language>
    <item>
      <title>SWIRL 5 is GA: knowledge authority for enterprise AI</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:04:12 +0000</pubDate>
      <link>https://dev.to/sidswirl/swirl-5-is-ga-knowledge-authority-for-enterprise-ai-5anp</link>
      <guid>https://dev.to/sidswirl/swirl-5-is-ga-knowledge-authority-for-enterprise-ai-5anp</guid>
      <description>&lt;p&gt;SWIRL 5 is generally available! I want to use this post to explain what it is at an engineering level, because the one-line pitch ("the knowledge authority layer for enterprise AI") does not tell you how it works or where the hard parts are. I would rather show you the machine.&lt;/p&gt;

&lt;p&gt;Quick disclosure: I wrote the original version of SWIRL and I run the company. So read this as the person who built it explaining the design, not as a neutral survey. I have tried to be honest about the limits, including where a hand-tuned stack matches us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that changed
&lt;/h2&gt;

&lt;p&gt;A year ago, "federated search across your systems, then re-rank the results" was a product you could sell. Today it is table stakes. MCP turned retrieval into a commodity: any model can reach any system through a connector, and every serious stack ships a re-ranker. If your pitch is "we retrieve and we re-rank," a technical evaluator will point at four other tools that do the same thing by lunch.&lt;/p&gt;

&lt;p&gt;So the interesting question moved. It is no longer "can you find the document." Everything finds the document. It is &lt;strong&gt;"which of the versions you found is the one my organization actually stands behind."&lt;/strong&gt; In a real enterprise corpus the answer to any given query does not exist once. It exists as a draft, three redlines, a copy someone saved to their desktop, and the executed final, all sitting in different systems, all semantically near-identical. Retrieval returns all of them. The model picks one, confidently, and it has no idea which one carries authority.&lt;/p&gt;

&lt;p&gt;That is a governance problem wearing a search costume, and it is what SWIRL 5 is built to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canonical version election
&lt;/h2&gt;

&lt;p&gt;The core primitive is version election. When SWIRL federates a query and gets back a cluster of near-identical documents, it does not just hand the pile to the model. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clusters the versions it found across every source.&lt;/li&gt;
&lt;li&gt;Scores each on signals that actually correlate with authority: &lt;strong&gt;source authority&lt;/strong&gt; (an executed contract in iManage outranks a draft on someone's OneDrive), &lt;strong&gt;naming&lt;/strong&gt; ("Executed", "Final", version numbers), and &lt;strong&gt;recency&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Elects one canonical version, and exposes its reasoning so a human can see why.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On top of that, teams can &lt;strong&gt;pin&lt;/strong&gt; a canonical result for a query directly. Once pinned, every later search and every agent calling SWIRL gets the endorsed answer, full stop. Election is the automatic path; pinning is the human override. Both produce the same thing: a single answer the organization has stood behind, not the model's best guess.&lt;/p&gt;

&lt;p&gt;This is the piece the frontier models cannot do for themselves. Claude and Copilot are extremely good at drafting and summarizing. Neither has any way to know which of your nine versions is ratified, because that fact does not live in the documents. It lives in your organization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ranking: three passes, run locally, no vector database
&lt;/h2&gt;

&lt;p&gt;Under the election sits the ranking pipeline. It runs in three passes, and both models run locally in your tenant. Nothing goes over the wire to a third-party ranking service, and there is no vector database to build, secure, or keep in sync.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keyword + BM25.&lt;/strong&gt; Lexical first. Quoted phrases and exact terms are honored as written. In enterprise and legal content this matters: a defined term or a specific clause has to match exactly, not approximately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding re-rank.&lt;/strong&gt; &lt;code&gt;E5-Large-V2&lt;/code&gt; embeddings with title-aware chunking, fused with the lexical scores using reciprocal rank fusion. Semantic recall, without letting it steamroll the exact matches from pass one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-encoder.&lt;/strong&gt; &lt;code&gt;MS-MARCO cross-encoder&lt;/code&gt; reads the query and each candidate document together, as a pair, and scores real relevance rather than vector similarity. It is the expensive pass, which is exactly why it runs last, only on the candidates that survived the first two.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The "no vector database" part is not a slogan, it is a deployment property. There is no second copy of your content sitting in an index that your information-governance policy never contemplated. The documents stay in iManage, Box, SharePoint, wherever they already live. Only the ranking happens in SWIRL. For a regulated buyer, removing that second copy removes an entire category of risk.&lt;/p&gt;

&lt;p&gt;If you want the independent version of this argument, Meta's XetHub benchmarked keyword-only, vector-only, and hybrid re-rank, and hybrid won. Their post was literally titled "you don't need a vector database."&lt;/p&gt;

&lt;h2&gt;
  
  
  Assembly: a bounded prompt, not a context dump
&lt;/h2&gt;

&lt;p&gt;Retrieval and ranking decide what is relevant. Assembly decides what the model actually sees, and that is where the token bill and a lot of the accuracy live.&lt;/p&gt;

&lt;p&gt;SWIRL treats the prompt as a hard budget, around 3,000 tokens, and fills it deliberately: at most 10 sources, only those scoring above a relevance threshold, a topic matcher that scores the spans &lt;em&gt;inside&lt;/em&gt; each source so only the passages that answer the query go in, per-source truncation to fit, and version de-duplication that drops the superseded copies before assembly. One bounded call, not the refine or tree-summarize modes that call the model once per chunk and multiply both tokens and latency.&lt;/p&gt;

&lt;p&gt;The counterintuitive result, which we measured against LangChain and LlamaIndex defaults and checked against their source: in a versioned corpus, sending &lt;em&gt;less&lt;/em&gt; produces a &lt;em&gt;better&lt;/em&gt; answer. More context means more near-identical duplicates, and the model gets less certain, not more. We wrote that study up separately if you want the numbers and the honest caveats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grounding checks
&lt;/h2&gt;

&lt;p&gt;Every generated answer is checked against the sources it cited. Claims that are not supported by the retrieved passages are flagged rather than shipped silently. This is not a hallucination cure, nothing is, but it changes the failure mode from "confident and wrong and unmarked" to "flagged for a human." In an enterprise setting that distinction is the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it fits your stack
&lt;/h2&gt;

&lt;p&gt;SWIRL 5 is headless and API-first, with a first-class MCP server. Any AI work surface calls it over MCP or REST and gets back ranked, permissioned, canonical answers with citations attached. It runs in your tenant, honors each source system's existing permissions on every query, and works with the model you choose, hosted Claude or GPT, Copilot, or a local model on your own hardware. The agent angle is the one I would watch: a human reading a superseded policy usually catches it, an agent does not pause, so serving agents the &lt;em&gt;approved&lt;/em&gt; answer instead of a raw retrieval is a safety property, not a nicety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is hard, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version election is heuristic.&lt;/strong&gt; Source authority, naming, and recency get you a long way, but naming conventions are messy and "most recent" is not always "operative." Pinning exists precisely because the automatic signal is not always enough. We show the reasoning so humans can correct it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A carefully hand-tuned vanilla RAG can match our per-call token count.&lt;/strong&gt; Low k, a good reranker, small chunks, plus your own de-dup layer. Our value is delivering that discipline by default and adding cross-version de-duplication that off-the-shelf stacks do not, not magic per-token compression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounding checks reduce unsupported claims, they do not eliminate them.&lt;/strong&gt; Treat the flag as a prompt for review, not a guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you are putting AI on your own data, that is exactly the case this is built for. It is generally available now at &lt;a href="https://swirlaiconnect.com" rel="noopener noreferrer"&gt;swirlaiconnect.com&lt;/a&gt;, and I am happy to run it against a slice of your own systems so you can see the ranking, the citations, and the permission boundary on your data rather than a demo corpus. I built it, so you would be talking to the person who wrote the code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>How many tokens does your RAG stack actually send to the LLM?</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Tue, 07 Jul 2026 13:13:00 +0000</pubDate>
      <link>https://dev.to/sidswirl/how-many-tokens-does-your-rag-stack-actually-send-to-the-llm-4hn6</link>
      <guid>https://dev.to/sidswirl/how-many-tokens-does-your-rag-stack-actually-send-to-the-llm-4hn6</guid>
      <description>&lt;p&gt;The token bill for a RAG system is not set by your vector database. It's set one step later, by how you assemble the context you hand the LLM. Retrieval finds candidates; &lt;strong&gt;assembly&lt;/strong&gt; decides how many of them, how much of each, and across how many LLM calls. That's where the money is spent.&lt;/p&gt;

&lt;p&gt;So "vector DB vs. framework X vs. SWIRL" is the wrong axis. The real comparison is between &lt;strong&gt;context-assembly strategies&lt;/strong&gt;. Here's an honest, source-checked look at how many tokens each common approach sends, and where SWIRL 5 actually costs less.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest headline first
&lt;/h2&gt;

&lt;p&gt;SWIRL doesn't win by using smaller chunks. Anyone can lower &lt;code&gt;top_k&lt;/code&gt;. On a single lean query, a minimal config like LlamaIndex's default (&lt;code&gt;top_k=2&lt;/code&gt;) sends &lt;em&gt;fewer&lt;/em&gt; raw tokens than SWIRL. If someone tells you SWIRL "always uses the fewest tokens," a technical evaluator will disprove it in five minutes.&lt;/p&gt;

&lt;p&gt;SWIRL's advantage is &lt;strong&gt;structural&lt;/strong&gt;, and it shows up exactly where enterprise content lives, in corpora full of document &lt;em&gt;versions&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It never spends tokens on duplicate or superseded versions.&lt;/strong&gt; Stock top-k returns whatever is nearest in embedding space, which in a versioned corpus means several near-identical copies. SWIRL collapses them to one canonical document &lt;em&gt;before&lt;/em&gt; the LLM sees them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It always answers in one bounded call.&lt;/strong&gt; The multi-document synthesis modes people reach for when they want quality (LangChain &lt;code&gt;refine&lt;/code&gt;/&lt;code&gt;map_reduce&lt;/code&gt;, LlamaIndex &lt;code&gt;refine&lt;/code&gt;/&lt;code&gt;tree_summarize&lt;/code&gt;) multiply LLM &lt;strong&gt;calls&lt;/strong&gt;, and &lt;code&gt;refine&lt;/code&gt; multiplies &lt;strong&gt;tokens&lt;/strong&gt; super-linearly. SWIRL never pays that tax.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the common defaults actually do
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Naive vector-DB RAG (Pinecone / Weaviate / Qdrant tutorials)
&lt;/h3&gt;

&lt;p&gt;The pattern is: embed the query → retrieve top-k chunks → stuff them all into one prompt. Typical documented defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chunk size &lt;strong&gt;~512 tokens&lt;/strong&gt; (the common "start here"; the band is 256-1024)&lt;/li&gt;
&lt;li&gt;Chunk overlap &lt;strong&gt;10-20%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;top_k&lt;/code&gt; &lt;strong&gt;3-5&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No de-duplication.&lt;/strong&gt; None of the vendor quickstarts add a dedup or diversity step. Overlap alone guarantees adjacent chunks share text, and multiple versions of a document sit in the same embedding neighborhood, so top-k routinely returns redundant context and the pipeline sends all of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Input tokens ≈ &lt;code&gt;k × chunk_tokens + overhead&lt;/code&gt;. At k=5 and ~1,000-token chunks, that's ~5,000 tokens, a large fraction of it redundant.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://www.pinecone.io/learn/chunking-strategies/" rel="noopener noreferrer"&gt;Pinecone&lt;/a&gt;, &lt;a href="https://weaviate.io/blog/chunking-strategies-for-rag" rel="noopener noreferrer"&gt;Weaviate&lt;/a&gt;, &lt;a href="https://qdrant.tech/documentation/tutorials-build-essentials/rag-deepseek/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  LangChain
&lt;/h3&gt;

&lt;p&gt;Verified from source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retriever default &lt;strong&gt;&lt;code&gt;k=4&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;similarity_search(k=4)&lt;/code&gt; in &lt;a href="https://github.com/langchain-ai/langchain/blob/master/libs/core/langchain_core/vectorstores/base.py" rel="noopener noreferrer"&gt;&lt;code&gt;vectorstores/base.py&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt; default &lt;strong&gt;4,000 chars / 200 overlap&lt;/strong&gt; (≈1,000 tokens) (&lt;a href="https://github.com/langchain-ai/langchain/blob/master/libs/text-splitters/langchain_text_splitters/base.py" rel="noopener noreferrer"&gt;&lt;code&gt;text_splitters/base.py&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The combine-documents chains differ enormously in cost:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chain&lt;/th&gt;
&lt;th&gt;LLM calls (N docs)&lt;/th&gt;
&lt;th&gt;Token behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stuff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;all docs in one prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map_reduce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N + ≥1&lt;/td&gt;
&lt;td&gt;one call per doc, then reduce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;N, sequential&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;re-sends the &lt;em&gt;growing&lt;/em&gt; answer each step → &lt;strong&gt;super-linear tokens&lt;/strong&gt;, no parallelism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map_rerank&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;one doc per call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;refine&lt;/code&gt; is the trap: each step re-transmits the accumulating answer plus the next document, so the running answer is re-sent N-1 times and grows as it goes.&lt;/p&gt;

&lt;h3&gt;
  
  
  LlamaIndex
&lt;/h3&gt;

&lt;p&gt;Verified from source (&lt;code&gt;llama-index-core&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chunk_size&lt;/code&gt; &lt;strong&gt;1,024 tokens&lt;/strong&gt;, &lt;code&gt;similarity_top_k&lt;/code&gt; &lt;strong&gt;2&lt;/strong&gt;, default response mode &lt;strong&gt;&lt;code&gt;compact&lt;/code&gt;&lt;/strong&gt; (&lt;a href="https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/constants.py" rel="noopener noreferrer"&gt;&lt;code&gt;constants.py&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/run-llama/llama_index/blob/main/llama-index-core/llama_index/core/response_synthesizers/factory.py" rel="noopener noreferrer"&gt;&lt;code&gt;factory.py&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No default de-duplication.&lt;/strong&gt; Node postprocessors are opt-in and the default similarity cutoff is off, so two versions of a doc in top-k both go to the LLM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Response modes (&lt;a href="https://developers.llamaindex.ai/python/framework/module_guides/deploying/query_engine/response_modes/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;LLM calls&lt;/th&gt;
&lt;th&gt;Token behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;compact&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;~1&lt;/td&gt;
&lt;td&gt;packs nodes into as few prompts as fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;one call per node, re-sends evolving answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tree_summarize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;1, recursive&lt;/td&gt;
&lt;td&gt;summarize groups, then summaries-of-summaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accumulate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;query each node separately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;compact&lt;/code&gt; with &lt;code&gt;top_k=2&lt;/code&gt; is genuinely lean, but that's a recall trade-off, and it still sends duplicate versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SWIRL 5 does (verified in source)
&lt;/h2&gt;

&lt;p&gt;SWIRL treats the LLM prompt as a hard budget and fills it deliberately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hard prompt budget&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RAG prompt capped at ~&lt;strong&gt;3,000 tokens&lt;/strong&gt; (&lt;code&gt;SWIRL_RAG_TOK_DEFAULT&lt;/code&gt;); assembly stops when full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source cap + relevance gate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;≤ &lt;strong&gt;10&lt;/strong&gt; sources, only those scoring &lt;strong&gt;≥ 0.8&lt;/strong&gt; are eligible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scored semantic chunks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a BM25 topic-matcher scores spans &lt;em&gt;within&lt;/em&gt; each source; on overflow SWIRL narrows to those scored spans, not the whole chunk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Truncation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;per-source token-by-token truncation to fit the budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Markup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;relevant spans wrapped in &lt;code&gt;&amp;lt;SW-IMPORTANT&amp;gt;…&amp;lt;/SW-IMPORTANT&amp;gt;&lt;/code&gt; plus a compact per-source metadata header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;De-dup before the LLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;version-cluster alternates dropped; if a doc is pinned, only the canonical is kept, so N versions collapse to 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single call&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;one stuff-style synthesis call (worst case +1 JSON repair)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Net: &lt;strong&gt;~3,000 input tokens, one call, zero redundant-version tokens&lt;/strong&gt;, a predictable ceiling that doesn't grow with document size or corpus size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting numbers on it
&lt;/h2&gt;

&lt;p&gt;Scenario: a query over a corpus where the relevant policy exists in &lt;strong&gt;5 versions&lt;/strong&gt;, plus 3 other relevant documents, a realistic enterprise shape. Total LLM &lt;strong&gt;input tokens per query&lt;/strong&gt; (summed across calls):&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%2F381ph9cic1pffq8tcuuw.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%2F381ph9cic1pffq8tcuuw.png" alt="Tokens sent to the LLM to answer one query: SWIRL vs. LangChain, LlamaIndex, naive vector RAG" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reading it honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vs. a &lt;strong&gt;deliberately minimal&lt;/strong&gt; stuff config (LlamaIndex &lt;code&gt;compact&lt;/code&gt;, &lt;code&gt;top_k=2&lt;/code&gt;): SWIRL is &lt;em&gt;comparable&lt;/em&gt; per call, but that config buys its low count with poor recall and still ships duplicate versions.&lt;/li&gt;
&lt;li&gt;vs. &lt;strong&gt;typical&lt;/strong&gt; stuff RAG (k=5, ~1k chunks): SWIRL is &lt;strong&gt;~40-45% fewer input tokens&lt;/strong&gt;, and it removes the redundant-version half entirely.&lt;/li&gt;
&lt;li&gt;vs. the &lt;strong&gt;quality-oriented multi-call modes&lt;/strong&gt; (&lt;code&gt;refine&lt;/code&gt;, &lt;code&gt;tree_summarize&lt;/code&gt;, &lt;code&gt;map_reduce&lt;/code&gt;): SWIRL is a &lt;strong&gt;2-3× reduction&lt;/strong&gt; in input tokens, and a larger reduction in &lt;strong&gt;output&lt;/strong&gt; tokens, since those modes generate once per call.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the advantage is real, and where it isn't
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real and hard to get off-the-shelf:&lt;/strong&gt; cross-&lt;strong&gt;version&lt;/strong&gt; de-duplication. None of these frameworks do it by default. In a versioned corpus it's the difference-maker, and it compounds: the more versions, the more SWIRL saves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real:&lt;/strong&gt; one bounded call vs. N-call synthesis; a predictable cost ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest caveat:&lt;/strong&gt; a hand-tuned vanilla RAG (low &lt;code&gt;k&lt;/code&gt;, a good reranker, small chunks, &lt;code&gt;stuff&lt;/code&gt;, plus your own dedup layer) &lt;em&gt;can&lt;/em&gt; match SWIRL's per-call token count. SWIRL's value is delivering that discipline &lt;strong&gt;by default&lt;/strong&gt;, and adding version de-dup the others lack. It isn't magic per-token compression.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're answering questions over a corpus with real document versioning, the tokens you waste aren't in chunk size - they're in &lt;strong&gt;sending the LLM the same document five times&lt;/strong&gt; and in &lt;strong&gt;synthesis modes that call the model once per chunk&lt;/strong&gt;. SWIRL 5's design removes both by default. Measure your own stack the same way: total input &lt;em&gt;and&lt;/em&gt; output tokens per query, summed across every LLM call. That number, not &lt;code&gt;top_k&lt;/code&gt;, is your bill.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Links + Snippets Not Enough for RAG</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Mon, 06 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/sidswirl/links-snippets-not-enough-for-rag-4mni</link>
      <guid>https://dev.to/sidswirl/links-snippets-not-enough-for-rag-4mni</guid>
      <description>&lt;p&gt;Several people posting lately about how RAG + search is not enough:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"All the model gets is a list of links and snippets. It's not enough to make sense of most real business questions."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Agreed. If your pipeline stuffs ten search snippets into a prompt and hopes, you get confident mush. A snippet is a pointer to evidence, not the evidence. The fix is a step most RAG setups skip: page fetch plus a reader. Three parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Fetch the page, not the snippet
&lt;/h2&gt;

&lt;p&gt;The search result is a pointer. So before generating, SWIRL fetches the actual page or document behind each top hit. Now the pipeline is working from the full source, not the 200 characters a search API happened to return.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Read it with a reader LLM
&lt;/h2&gt;

&lt;p&gt;This is the part that matters. Some model reads each fetched page against the question and pulls out the passages that actually answer it, marking and scoring them. The chaff never reaches the expensive answering model. &lt;/p&gt;

&lt;p&gt;Do extraction before generation, not instead of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Budget the context
&lt;/h2&gt;

&lt;p&gt;Real pages do not fit neatly into a context window, so the reader either truncates to the highest-signal passages or marks them in place, best first, until the budget is spent. The answering model gets curated, ranked evidence instead of a pile of chunks competing for attention.&lt;/p&gt;

&lt;p&gt;The result is the difference between "here are some links" and an answer you can act on. Same retrieval, far better grounding, because something actually read the sources before the model spoke.&lt;/p&gt;

&lt;p&gt;That take is exactly why the reader step exists. Naive RAG earns its bad reputation. This is how you avoid it.&lt;/p&gt;

&lt;p&gt;(And don't get me started about not having to put it all in a vector database first!)&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Making RAG admit when it's guessing: source-grounded hallucination checks</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Wed, 01 Jul 2026 15:12:48 +0000</pubDate>
      <link>https://dev.to/sidswirl/making-rag-admit-when-its-guessing-source-grounded-hallucination-checks-g22</link>
      <guid>https://dev.to/sidswirl/making-rag-admit-when-its-guessing-source-grounded-hallucination-checks-g22</guid>
      <description>&lt;p&gt;The failure mode that scares me most in RAG isn't a wrong answer. It's a confident wrong answer with three citations that don't actually say what the answer claims.&lt;/p&gt;

&lt;p&gt;So in SWIRL 5 I stopped trusting the model to police itself and added a check that runs &lt;em&gt;after&lt;/em&gt; generation.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate the answer with its citations, as usual.&lt;/li&gt;
&lt;li&gt;Split the answer into atomic claims — roughly one assertion per sentence.&lt;/li&gt;
&lt;li&gt;For each claim, pull the specific spans from the retrieved passages the model cited.&lt;/li&gt;
&lt;li&gt;Run an entailment check: does the cited text actually support this claim, contradict it, or neither?&lt;/li&gt;
&lt;li&gt;Any claim that isn't supported gets flagged in the UI, inline, before the user reads a word of it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The interesting part wasn't the entailment model; it was everything around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claim segmentation is harder than it sounds.&lt;/strong&gt; Naive sentence splitting produces claims that are unverifiable on their own because the subject lives two sentences up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Citations lie by omission.&lt;/strong&gt; A model will cite a document that's &lt;em&gt;topically&lt;/em&gt; relevant but doesn't contain the specific number it just quoted. The whole point of the check is to catch exactly that gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency budget.&lt;/strong&gt; An honesty layer nobody waits for is an honesty layer nobody ships. SWIRL 5 batches and optionally caches passage embeddings and more. &lt;/p&gt;

&lt;p&gt;The result isn't "SWIRL never hallucinates." Nothing can promise that. The result is: when it's on thin ice, it tells you, and it points at the exact sentence.&lt;/p&gt;

&lt;p&gt;That's the version of trustworthy I can actually build.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
    </item>
    <item>
      <title>SWIRL Community 4.5 Update</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:30:28 +0000</pubDate>
      <link>https://dev.to/sidswirl/swirl-community-45-update-3bji</link>
      <guid>https://dev.to/sidswirl/swirl-community-45-update-3bji</guid>
      <description>&lt;p&gt;I wrote about SWIRL here last summer. Time for an update.&lt;/p&gt;

&lt;p&gt;SWIRL Community 4.5 is out. It is the open-source, Apache-2.0 build of SWIRL: federated search and RAG across your apps, running on your own machine, no vector database required. Three things in this release are worth your attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Point RAG at any LLM
&lt;/h2&gt;

&lt;p&gt;The big one. Community now lets you &lt;a href="https://docs.swirlaiconnect.com/rag-guide#configuring-rag" rel="noopener noreferrer"&gt;configure AI Providers&lt;/a&gt; and send your RAG queries to any model you want: OpenAI, Anthropic, Azure OpenAI, or a fully local model behind Ollama or vLLM. Pick the provider, set the model, done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ai_provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anthropic&lt;/span&gt;      &lt;span class="c1"&gt;# openai | azure | ollama | vllm&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-model&lt;/span&gt;
  &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${LLM_KEY}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for two reasons. RAG quality is now your choice, not ours, so you can run a stronger model when accuracy counts. And you can keep the whole pipeline local, with retrieval and generation both inside your network. This was an Enterprise feature; it is in Community as of 4.5.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ask RAG a question directly
&lt;/h2&gt;

&lt;p&gt;RAG now accepts a natural-language question as its input. Before, you ran a search and generated over the results; now you can hand SWIRL a plain question and it does retrieval and generation in one step. Question in, cited answer out, which means a lot less glue code to wire SWIRL into a chat box or an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Updated Galaxy
&lt;/h2&gt;

&lt;p&gt;Community now includes the new Galaxy 5 UI, including a search history widget + dashboard, cleaned up admin interface, built-in activity analytics and more. &lt;/p&gt;

&lt;p&gt;That is the release. Free, self-hosted, and now model-agnostic.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>nlp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The knowledge-authority layer: what your agents can't get from the outside</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Wed, 17 Jun 2026 13:28:55 +0000</pubDate>
      <link>https://dev.to/sidswirl/the-knowledge-authority-layer-what-your-agents-cant-get-from-the-outside-f4i</link>
      <guid>https://dev.to/sidswirl/the-knowledge-authority-layer-what-your-agents-cant-get-from-the-outside-f4i</guid>
      <description>&lt;p&gt;Every enterprise AI conversation right now starts in the same place: "connect the model to our data." Then it stalls in the same place: &lt;em&gt;which&lt;/em&gt; data, copied &lt;em&gt;where&lt;/em&gt;, governed by &lt;em&gt;whom&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let me make an argument that runs against the current default - and then show the architecture it implies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default is a second copy of your data
&lt;/h2&gt;

&lt;p&gt;The standard RAG recipe is: crawl your sources, chunk them, embed them, and load the vectors into a database. Now your model can retrieve. It also means you have a &lt;em&gt;second copy&lt;/em&gt; of your content living in an index you have to secure, keep in sync, and explain to whoever owns compliance. You've recreated every permission boundary by hand, and you'll eventually get one wrong.&lt;/p&gt;

&lt;p&gt;For a lot of teams that copy is simply not allowed. Regulated content, client-confidential material, anything privileged - copying it into a vendor store is exposure you don't get paid to take on.&lt;/p&gt;

&lt;h2&gt;
  
  
  You probably don't need the vector database
&lt;/h2&gt;

&lt;p&gt;Here's the part people don't want to hear. Meta's XetHub team &lt;a href="https://web.archive.org/web/20251221204717/https://xethub.com/blog/you-dont-need-a-vector-database" rel="noopener noreferrer"&gt;benchmarked three retrieval strategies&lt;/a&gt;: keyword-only (BM25), vector-only, and hybrid (keyword to pull candidates, then re-rank). Keyword-only came last. Vector-only did better. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid won&lt;/strong&gt; - and their conclusion was blunt: "No vector database necessary."&lt;/p&gt;

&lt;p&gt;That matches what we see in production. Vector similarity is a great &lt;em&gt;high-precision filter&lt;/em&gt;, not a great &lt;em&gt;first pass&lt;/em&gt;. Lead with exact matches and quoted terms, then let embeddings and a cross-encoder re-rank what's left.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "make your LLM better" actually means
&lt;/h2&gt;

&lt;p&gt;It's not a slogan; it's a pipeline. In SWIRL, relevance is three passes, and both models run locally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Federate and match.&lt;/strong&gt; Query every connected source in parallel - keyword + BM25 - and honor quoted phrases and exact terms first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding re-rank.&lt;/strong&gt; Re-rank candidates with &lt;code&gt;E5-large-v2&lt;/code&gt;, using title-aware chunking and hybrid keyword+vector fusion (RRF). No vector database to build or secure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-encoder re-rank.&lt;/strong&gt; An &lt;code&gt;MS-MARCO&lt;/code&gt; cross-encoder reads the query and document &lt;em&gt;together&lt;/em&gt; and scores real relevance, not vector distance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Feed &lt;em&gt;that&lt;/em&gt; to your LLM - whatever model you've chosen, including an on-prem one - and the answer gets better, because the context got better. Same model, sharper input.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layer no model supplies from the outside
&lt;/h2&gt;

&lt;p&gt;The stack is settling: foundation models orchestrate, MCP is the retrieval interface, the chat UI is a commodity. The piece none of them provide from outside your walls is &lt;strong&gt;knowledge authority&lt;/strong&gt; - which document is official, which clause your org actually uses, which answer carries approval.&lt;/p&gt;

&lt;p&gt;So we made it a first-class layer. SWIRL 5 exposes an MCP server. Any agent - Claude, Copilot, ChatGPT, your own - calls SWIRL and gets ranked, permissioned, &lt;em&gt;organization-approved&lt;/em&gt; answers. A team pins the canonical result for a query once; every agent gets it after that. And no copy of your data leaves your tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this shape
&lt;/h2&gt;

&lt;p&gt;Three properties fall out of it, and they're the whole reason to build it this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private by architecture.&lt;/strong&gt; Data stays in place; permissions are enforced live; there's no second index to govern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The answer, not a guess.&lt;/strong&gt; Cross-encoder ranking plus canonical answers means people and agents get the result the org trusts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The safe on-ramp to AI.&lt;/strong&gt; Headless and MCP-native, deployed in your tenant - the lowest-risk way to give agents enterprise reach.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're wiring agents into enterprise data and the "just copy everything into a vector store" step is making your security team twitch, there's another shape available. SWIRL 5 goes GA July 15; the preview is open if you want to point it at your own stack. Either way - I'd genuinely like to hear how you're handling the authority problem, because I don't think the industry has it figured out yet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sid Probstein is the creator of SWIRL and CEO of SWIRL AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Moving Docker images between repos with crane (after imagetools wasted my afternoon)</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Thu, 11 Jun 2026 17:55:15 +0000</pubDate>
      <link>https://dev.to/sidswirl/moving-docker-images-between-repos-with-crane-after-imagetools-wasted-my-afternoon-bjg</link>
      <guid>https://dev.to/sidswirl/moving-docker-images-between-repos-with-crane-after-imagetools-wasted-my-afternoon-bjg</guid>
      <description>&lt;p&gt;I had a freshly-built, multi-arch dev image (&lt;code&gt;linux/amd64&lt;/code&gt; + &lt;code&gt;linux/arm64&lt;/code&gt;) and one job: promote it into a private partner repo on Docker Hub, plus stamp a dated tag so &lt;code&gt;:latest&lt;/code&gt; is always traceable back to a real build. Cross-repo. Should be five minutes.&lt;/p&gt;

&lt;p&gt;It was not five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;The obvious tool is &lt;code&gt;docker buildx imagetools create&lt;/code&gt;... it's built for copying manifests between tags. So I reached for it. And it sat there. Then it 400'd. Then I retried, and it hung. Cross-repo blob copies on Docker Hub are reproducibly flaky with imagetools, and I burned the better part of an hour confirming that before I went looking for something else.&lt;/p&gt;

&lt;p&gt;The fallback most people reach for next is worse: pull the image down, retag it, push it to the new repo. That round-trips the &lt;em&gt;entire&lt;/em&gt; image — every layer, every arch - through your laptop's daemon and disk, only to push the same bits back up. And if you're not careful with how you tag, you flatten a multi-arch index down to whatever single arch your machine happens to be. No thanks.&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%2Fxnetefv8hsa8yrjqd3ix.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%2Fxnetefv8hsa8yrjqd3ix.png" alt=" " width="799" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why crane
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/google/go-containerregistry/tree/main/cmd/crane" rel="noopener noreferrer"&gt;&lt;code&gt;crane&lt;/code&gt;&lt;/a&gt; (from Google's go-containerregistry) does the copy &lt;strong&gt;registry-to-registry&lt;/strong&gt;. It never pulls the image to your machine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It preserves the multi-arch manifest.&lt;/strong&gt; &lt;code&gt;crane cp&lt;/code&gt; copies the whole image &lt;em&gt;index&lt;/em&gt; by digest. Both arches come along. Nothing gets flattened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It actually works cross-repo on Docker Hub.&lt;/strong&gt; Where imagetools 400'd and hung, crane did the server-to-server copy in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No daemon, no disk.&lt;/strong&gt; It talks to the registries directly. Your laptop just orchestrates.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph slow["pull → tag → push"]
        A[source repo&amp;lt;br/&amp;gt;you/app:dev] --&amp;gt; L[your daemon + disk&amp;lt;br/&amp;gt;whole image] --&amp;gt; B[partner repo&amp;lt;br/&amp;gt;you/partner-app:latest]
    end
    subgraph crane["crane cp"]
        C[source repo&amp;lt;br/&amp;gt;you/app:dev] --&amp;gt;|manifest + blobs&amp;lt;br/&amp;gt;by digest| D[partner repo&amp;lt;br/&amp;gt;you/partner-app:latest]
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run it as a container, no install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; gcr.io/go-containerregistry/crane:debug &amp;lt;crane-args&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole installation story. Nothing on the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth issues (MacOS)
&lt;/h2&gt;

&lt;p&gt;crane in the container reads &lt;code&gt;~/.docker/config.json&lt;/code&gt;. On &lt;strong&gt;macOS with Docker Desktop&lt;/strong&gt;, your login isn't &lt;em&gt;in&lt;/em&gt; that file — it's &lt;code&gt;credsStore: osxkeychain&lt;/code&gt;, sitting in the macOS keychain. So if you naively mount &lt;code&gt;~/.docker/config.json&lt;/code&gt; into the container, crane sees no usable credential and hands you &lt;code&gt;UNAUTHORIZED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix: pull the credential out of the keychain on the host, write it into a &lt;em&gt;temporary inline&lt;/em&gt; config, mount that, and delete it the moment you're done. Never print it, never commit it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf "$TMP"'&lt;/span&gt; EXIT
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/mkcfg.py"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import json, sys, base64
d = json.load(sys.stdin)
auth = base64.b64encode((d["Username"] + ":" + d["Secret"]).encode()).decode()
json.dump({"auths": {"https://index.docker.io/v1/": {"auth": auth}}}, open(sys.argv[1], "w"))
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"https://index.docker.io/v1/"&lt;/span&gt; | docker-credential-osxkeychain get | python3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/mkcfg.py"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/config.json"&lt;/span&gt;

CRANE&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;/config.json"&lt;/span&gt;:/root/.docker/config.json:ro &lt;span class="se"&gt;\&lt;/span&gt;
            gcr.io/go-containerregistry/crane:debug &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;trap ... EXIT&lt;/code&gt; cleans up the temp config when your shell exits, so the credential doesn't linger.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;Linux&lt;/strong&gt; (or anywhere &lt;code&gt;docker login&lt;/code&gt; writes inline creds), skip all of that and mount &lt;code&gt;~/.docker/config.json&lt;/code&gt; directly. And obvious-but-worth-saying: you need a &lt;strong&gt;read/write&lt;/strong&gt; login on the destination. A read-only token can't push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promote the image
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# copy SRC -&amp;gt; DST, full multi-arch manifest, server to server:&lt;/span&gt;
CRANE &lt;span class="nb"&gt;cp &lt;/span&gt;you/app:dev &lt;span class="se"&gt;\&lt;/span&gt;
         you/partner-app:latest

&lt;span class="c"&gt;# also stamp a dated, immutable tag so :latest is always traceable to a build:&lt;/span&gt;
CRANE &lt;span class="nb"&gt;cp &lt;/span&gt;you/app:dev &lt;span class="se"&gt;\&lt;/span&gt;
         you/partner-app:dev-2026-06-11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two copies, both registry-side, both done before you can refill your coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;Don't trust that the copy worked. Prove it. The destination digest must equal the source digest — same bits, same manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CRANE digest you/app:dev                 &lt;span class="c"&gt;# source&lt;/span&gt;
CRANE digest you/partner-app:latest      &lt;span class="c"&gt;# must equal source&lt;/span&gt;
CRANE digest you/partner-app:dev-2026-06-11   &lt;span class="c"&gt;# must equal source&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those three lines match, the right image landed under both tags. If they don't, you copied the wrong thing — better to find out here than in a partner's deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CRANE &lt;span class="nb"&gt;ls &lt;/span&gt;you/partner-app                          &lt;span class="c"&gt;# list tags&lt;/span&gt;
CRANE manifest you/app:dev                         &lt;span class="c"&gt;# full manifest JSON, see the arches&lt;/span&gt;
CRANE tag you/app@sha256:&amp;lt;digest&amp;gt; newtag           &lt;span class="c"&gt;# add a tag to an existing digest&lt;/span&gt;
CRANE copy &amp;lt;SRC&amp;gt; &amp;lt;DST&amp;gt;                             &lt;span class="c"&gt;# alias of cp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat worth knowing: tag &lt;em&gt;deletion&lt;/em&gt; isn't a crane operation. Use the Docker Hub UI or the API for that (and it needs an admin-scoped token — a read/write PAT won't delete).&lt;/p&gt;

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

&lt;p&gt;Reach for crane any time you're moving an image &lt;em&gt;between registries or repos&lt;/em&gt; and you care about the manifest arriving intact — promotions, mirrors, cross-org handoffs. It skips the daemon, skips the disk, and it doesn't fall over on Docker Hub cross-repo copies the way imagetools does. And whatever you do, build the &lt;code&gt;crane digest&lt;/code&gt; source-equals-dest check into the workflow. It costs two seconds.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>resources</category>
    </item>
    <item>
      <title>Running PyTorch fork-safe in Celery on macOS</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Mon, 01 Jun 2026 13:23:46 +0000</pubDate>
      <link>https://dev.to/sidswirl/running-pytorch-fork-safe-in-celery-on-macos-4h6a</link>
      <guid>https://dev.to/sidswirl/running-pytorch-fork-safe-in-celery-on-macos-4h6a</guid>
      <description>&lt;p&gt;If you've ever seen this in your Celery logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Process 'ForkPoolWorker-7' pid:32839 exited with 'signal 11 (SIGSEGV)'
billiard.exceptions.WorkerLostError:
    Worker exited prematurely: signal 11 (SIGSEGV) Job: 0.

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

&lt;/div&gt;



&lt;p&gt;...and the macOS crash report buries the real message in a JSON blob:&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="nl"&gt;"asi"&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;"CoreFoundation"&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="s2"&gt;"*** multi-threaded process forked ***"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"libsystem_c.dylib"&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="s2"&gt;"crashed on child side of fork pre-exec"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...you've hit one of the classic fork-after-init traps. Here's what's going on and how to actually fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line fix (if you're in a hurry)
&lt;/h2&gt;

&lt;p&gt;Set these env vars before the Celery worker's MainProcess imports anything heavy:&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="c1"&gt;# &amp;lt;your-project&amp;gt;/celery.py ... the very first thing the worker imports
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENBLAS_NUM_THREADS&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;OMP_NUM_THREADS&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;MKL_NUM_THREADS&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;NUMEXPR_NUM_THREADS&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;VECLIB_MAXIMUM_THREADS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OBJC_DISABLE_INITIALIZE_FORK_SAFETY&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;YES&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 first set forces every BLAS library to single-threaded mode. VECLIB_MAXIMUM_THREADS is the one most people forget; it covers Apple's Accelerate framework, which is what PyTorch uses by default on Apple Silicon. The last one tells the Objective-C runtime to skip its fork-safety abort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;PyTorch's nn.Linear on macOS arm64 calls into Apple Accelerate, which does its parallel matmuls via libdispatch (Grand Central Dispatch).&lt;/p&gt;

&lt;p&gt;The first BLAS call lazily spins up a pool of libdispatch worker queues in the calling process.&lt;/p&gt;

&lt;p&gt;If that "calling process" is your Celery worker's MainProcess (say, because something during boot does a tiny matmul: spaCy preload, an embedding warmup, anything that imports numpy and runs a real op), those queues now live in the parent. &lt;/p&gt;

&lt;p&gt;When the prefork pool then fork()s a child, the child inherits broken queue handles. The next BLAS call from inside the child dereferences a stale pointer and you get the SIGSEGV.&lt;/p&gt;

&lt;p&gt;The stack trace in the crash report makes it unambiguous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0: _dispatch_apply_with_attr_f      (libdispatch)
1: dispatch_apply_with_attr         (libdispatch)
3: cblas_sgemm                      (Accelerate)
5: at::native::cpublas::gemm        (libtorch_cpu)
6: at::native::addmm_impl_cpu_      (libtorch_cpu)
7: at::native::linear               (libtorch_cpu)
8: torch::autograd::THPVariable_linear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What doesn't work
&lt;/h2&gt;

&lt;p&gt;"Just lazy-load the model in the child." Even if you defer from_pretrained until you're inside a forked child, that first call still hits Accelerate BLAS, and the dispatch queues your child inherited from the parent are already broken.&lt;/p&gt;

&lt;p&gt;"Just bypass sentence_transformers.CrossEncoder.predict() and use bare-torch." Same story. Whether you go through CrossEncoder or call AutoModelForSequenceClassification directly, the SIGSEGV is one frame down inside linear().&lt;/p&gt;

&lt;p&gt;"Just don't import torch at the top of the module." Necessary but not sufficient. In our case, removing import torch from ai_provider.py was real progress, but then we discovered litellm transitively pulls torch the first time you call it. Every "warmup" preload that touched litellm still poisoned the parent. You have to audit every code path that runs before the first fork.&lt;/p&gt;

&lt;h2&gt;
  
  
  The defensive pattern that does work
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Defer heavy imports. Don't import torch at module top in anything that's part of the Celery autodiscovery chain. Push it into the function that needs it:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bad ... taints anyone who imports this module
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;documents&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;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# Good ... torch only loads in workers that actually rerank
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Gate "warmup" preloads off the Celery worker. Preloading models at startup makes sense for an ASGI server like Daphne. It is actively harmful in a forking Celery worker, because the warmup runs in MainProcess:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyAppConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppConfig&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;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;is_celery_worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celery&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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;is_celery_worker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_preload_cross_encoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What about Linux / Docker?
&lt;/h2&gt;

&lt;p&gt;Yes, this affects Linux too, just less dramatically. &lt;/p&gt;

&lt;p&gt;OpenBLAS and MKL both spin up thread pools on first use that don't survive fork; the typical Linux failure mode is a hang or a deadlock rather than a SIGSEGV. &lt;/p&gt;

&lt;p&gt;The good news: the same *_NUM_THREADS=1 env vars are the fix. &lt;/p&gt;

&lt;p&gt;VECLIB_MAXIMUM_THREADS and OBJC_DISABLE_INITIALIZE_FORK_SAFETY are no-ops on Linux, so the snippet above is portable. The deferred-import and gate-off-warmup patterns apply unchanged.&lt;/p&gt;

</description>
      <category>python</category>
      <category>celery</category>
    </item>
    <item>
      <title>The AI agent stack that’s quietly taking over enterprise workflows</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Sat, 03 May 2025 02:58:27 +0000</pubDate>
      <link>https://dev.to/sidswirl/the-ai-agent-stack-thats-quietly-taking-over-enterprise-workflows-2h8o</link>
      <guid>https://dev.to/sidswirl/the-ai-agent-stack-thats-quietly-taking-over-enterprise-workflows-2h8o</guid>
      <description>&lt;p&gt;Accenture, IBM, and AWS are all placing bets on &lt;a href="https://www.crewai.com/" rel="noopener noreferrer"&gt;Crew AI&lt;/a&gt;. Why? Because it makes building and deploying real AI agents possible.&lt;/p&gt;

&lt;p&gt;With Crew AI, teams are spinning up agents that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Launch predictive marketing campaigns&lt;/li&gt;
&lt;li&gt;Automate financial back-office ops&lt;/li&gt;
&lt;li&gt;Optimize inventory and logistics&lt;/li&gt;
&lt;li&gt;And tackle 100+ other enterprise use cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here’s the catch: agents are only as good as the data they can reach. That’s where &lt;a href="https://swirlaiconnect.com/" rel="noopener noreferrer"&gt;SWIRL&lt;/a&gt; comes in.&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%2Fbo016mxcb9nx03auythy.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%2Fbo016mxcb9nx03auythy.png" alt=" " width="800" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By pairing Crew AI with SWIRL, you get more than just agents—you get enterprise-ready, data-rich workflows that scale. No custom plumbing. No brittle integrations.&lt;/p&gt;

&lt;p&gt;With Crew AI + SWIRL, your agents can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect to &lt;a href="https://swirlaiconnect.com/connectors" rel="noopener noreferrer"&gt;100+ enterprise data sources&lt;/a&gt; out-of-the-box&lt;/li&gt;
&lt;li&gt;Fetch the most relevant structured/unstructured data across silos&lt;/li&gt;
&lt;li&gt;Respect row-level permissions with real enterprise auth&lt;/li&gt;
&lt;li&gt;Summarize and answer with your LLM of choice&lt;/li&gt;
&lt;li&gt;Plug in easily via zero-code connectors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want to see this in action? &lt;/p&gt;

&lt;p&gt;Message me for a demo or check the open source edition here: &lt;a href="https://github.com/swirlai/swirl-search" rel="noopener noreferrer"&gt;https://github.com/swirlai/swirl-search&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>showdev</category>
    </item>
    <item>
      <title>It was already indexed!</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Sun, 13 Aug 2023 16:48:52 +0000</pubDate>
      <link>https://dev.to/sidswirl/it-was-already-indexed-2b4</link>
      <guid>https://dev.to/sidswirl/it-was-already-indexed-2b4</guid>
      <description>&lt;p&gt;I recently had the pleasure of chatting with &lt;a class="mentioned-user" href="https://dev.to/dmitrykan"&gt;@dmitrykan&lt;/a&gt; on his  &lt;a href="https://dmitry-kan.medium.com/vector-podcast-e27d83ecd0be" rel="noopener noreferrer"&gt;Vector Podcast&lt;/a&gt;. Check it out: &lt;a href="https://dmitry-kan.medium.com/vector-podcast-with-sid-probstein-search-in-siloed-data-with-swirl-f2b9595a2715" rel="noopener noreferrer"&gt;https://dmitry-kan.medium.com/vector-podcast-with-sid-probstein-search-in-siloed-data-with-swirl-f2b9595a2715&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We talked about quite a few things, including: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The challenges of enterprise search in the post-cloud era&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How cross-silo search is particularlyt tricky because of entitlements (aka permissions) across silos&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Zero-code configuration of connectors in Swirl, where JSON path and developer API doc get the job done &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How large language models contextually re-rank disparate search results&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There was an interesting twist at the end of the call. Dmitry uses a service called &lt;a href="https://clearword.com/" rel="noopener noreferrer"&gt;Clearword&lt;/a&gt; to transcribe recordings. Dmitry asked: “how quickly can you index the transcript and search it with Swirl?” &lt;/p&gt;

&lt;p&gt;Here is my answer: &lt;a href="https://www.youtube.com/watch?v=WMHnb_6Wf50" rel="noopener noreferrer"&gt;It was already indexed!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/WMHnb_6Wf50"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Since there is no audio and it goes by quickly, let me explain ... Clearword emailed the transcript to both of us shortly after recording ended. It was indexed by Microsoft Outlook within seconds of arriving in my inbox. &lt;/p&gt;

&lt;p&gt;To verify this, I copied some text from the middle of the transcript and pasted it into Swirl, which returned the link to the email message with the transcript and the phrase I searched for. &lt;/p&gt;

&lt;p&gt;That simple truth - that the average enterprise is awash in search forms - is the entire reason metasearch is such a game-changing approach. Instead of making yet another repository, Swirl sends queries to existing search APIs and re-ranks the results from everything. It saves users a huge amount of time without a major IT project. &lt;/p&gt;

&lt;p&gt;Want to see for yourself? git it going with 2 commands via Docker here: &lt;a href="https://github.com/swirlai/swirl-search" rel="noopener noreferrer"&gt;https://github.com/swirlai/swirl-search&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Swirl 2.5 released</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Wed, 09 Aug 2023 14:37:03 +0000</pubDate>
      <link>https://dev.to/sidswirl/swirl-25-released-40g4</link>
      <guid>https://dev.to/sidswirl/swirl-25-released-40g4</guid>
      <description>&lt;p&gt;&lt;strong&gt;I am delighted to announce availability of Swirl 2.5!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi78nz5551etm2kuo0j5v.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%2Fi78nz5551etm2kuo0j5v.png" alt=" " width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This version focused on performance. Configured with 12 SearchProviders, Swirl 2.5 supports ~15 queries/second on a Standard F16s v2 server (16 vcpus, 32 GiB memory) with a median response time of ~3 seconds. &lt;/p&gt;

&lt;p&gt;Version 2.5 also includes SearchProviders for HubSpot contact, company, and deal records, plus improvements to the Galaxy search UI (shown above).&lt;/p&gt;

&lt;p&gt;Check out the Release notes for full details: &lt;a href="https://github.com/swirlai/swirl-search/releases/tag/v2.5.0" rel="noopener noreferrer"&gt;https://github.com/swirlai/swirl-search/releases/tag/v2.5.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Swirl?&lt;/strong&gt; A new open source metasearch engine; it queries anything with an API then uses spaCy to re-rank the unified results without copying any data! Includes zero-code configs for Apache Solr, ChatGPT, Elastic Search, OpenSearch, PostgreSQL, Google BigQuery, RequestsGet, Google PSE, NLResearch.com, Miro, Microsoft 365, HubSpot, Atlassian, YouTrack, GitHub &amp;amp; more! &lt;/p&gt;

</description>
      <category>search</category>
      <category>python</category>
      <category>django</category>
    </item>
    <item>
      <title>I wrote a metasearch engine called Swirl</title>
      <dc:creator>Sid Probstein</dc:creator>
      <pubDate>Thu, 03 Aug 2023 18:48:40 +0000</pubDate>
      <link>https://dev.to/sidswirl/i-wrote-a-metasearch-engine-called-swirl-23i9</link>
      <guid>https://dev.to/sidswirl/i-wrote-a-metasearch-engine-called-swirl-23i9</guid>
      <description>&lt;p&gt;Hi all! &lt;a href="https://github.com/sidprobstein/swirl-search" rel="noopener noreferrer"&gt;Swirl&lt;/a&gt; sends queries to existing search engines, unifies the results and re-ranks them all using large language models. It solves cross-silo information access and search problems in a fraction of the time and effort required to copy, ingest and index data. &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%2Fgo4d13eov2335nmcxjy3.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%2Fgo4d13eov2335nmcxjy3.png" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a brief video intro: &lt;a href="https://youtu.be/sfsBYyu6qDQ" rel="noopener noreferrer"&gt;https://youtu.be/sfsBYyu6qDQ&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Swirl was written in python atop the django/celery/redis stack with a choice of Sqlite3 or PostgreSQL back-ends. The source code is available under the Apache 2.0 license. The distribution includes zero-code configs for Apache Solr, ChatGPT, Elastic Search, OpenSearch, PostgreSQL, Google BigQuery, RequestsGet, Google PSE, NLResearch.com, Miro, Microsoft 365, Atlassian, YouTrack, GitHub, HubSpot &amp;amp; more. Plug-in your access tokens or use Microsoft 365 to login and users can stop searching and start Swirling!&lt;/p&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sidprobstein/swirl-search" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.swirl.today" rel="noopener noreferrer"&gt;Swirl website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://join.slack.com/t/swirlmetasearch/shared_invite/zt-1qk7q02eo-kpqFAbiZJGOdqgYVvR1sfw" rel="noopener noreferrer"&gt;Slack channel for support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are seeking contributions and feedback from developers working on all kinds of search solutions... thanks!&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%2Fgl07sutgaxaulu25xvqm.jpg" 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%2Fgl07sutgaxaulu25xvqm.jpg" alt="Image description" width="800" height="683"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>search</category>
      <category>cloudcomputing</category>
      <category>llm</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
