<?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: Chaos Cypher</title>
    <description>The latest articles on DEV Community by Chaos Cypher (chaoscypherinc).</description>
    <link>https://dev.to/chaoscypherinc</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%2Forganization%2Fprofile_image%2F14629%2F84ef867c-e8ab-4fb7-bdf9-4d565029de3f.png</url>
      <title>DEV Community: Chaos Cypher</title>
      <link>https://dev.to/chaoscypherinc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chaoscypherinc"/>
    <language>en</language>
    <item>
      <title>Citation Click-Through: How Chaos Cypher Traces Every Answer to Its Source</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:51:36 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/citation-click-through-how-chaos-cypher-traces-every-answer-to-its-source-404i</link>
      <guid>https://dev.to/chaoscypherinc/citation-click-through-how-chaos-cypher-traces-every-answer-to-its-source-404i</guid>
      <description>&lt;p&gt;Most AI chat answers ask you to take them on faith. There's no way to tell which sentence came from your documents and which one the model filled in from its own training. Chaos Cypher takes a different position: every claim in a chat answer that's grounded in your knowledge graph carries a citation, and every citation is a link you can click.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Black-Box Answers
&lt;/h2&gt;

&lt;p&gt;Ask a chatbot a question about your own documents and you get an answer that reads well and might be completely right -- or quietly wrong in a way you'd only catch by re-reading the source yourself. The confidence of the answer tells you nothing about how well-grounded it is. That gap between "sounds right" and "is right" is the core trust problem with RAG-backed chat, and it only gets worse as your knowledge graph grows past what you can hold in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Click-Through Looks Like
&lt;/h2&gt;

&lt;p&gt;Ask Chaos Cypher a question in &lt;strong&gt;Chat&lt;/strong&gt; and the streamed answer arrives with small inline markers next to the sentences it pulled from your sources -- a document icon for text, a thumbnail for an image chunk. Hover one and a tooltip shows the source it came from, the page number, and a validation badge: &lt;strong&gt;Verified&lt;/strong&gt; when the cited sentence was actually found in that chunk, &lt;strong&gt;Invalid&lt;/strong&gt; when it wasn't.&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%2Fax02jopz0ihmszz4b587.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%2Fax02jopz0ihmszz4b587.png" alt="Chat page with an AI answer ranking the top entities by PageRank, with entity chips" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the marker and you land on that source's detail page with the exact chunk highlighted -- not "somewhere in this document," the specific paragraph the sentence was grounded in. Answers that pulled from a scanned page or image work the same way: the marker is a thumbnail, and clicking it opens the full 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58w00hqzu0lp1cdoj8iw.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%2F58w00hqzu0lp1cdoj8iw.png" alt="Source overview for war_and_peace.txt: entity and relationship counts, pipeline flow, entity distribution" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters More Than a Nice-to-Have
&lt;/h2&gt;

&lt;p&gt;This closes the loop that most RAG chat interfaces leave open. Instead of trusting a generated answer wholesale, you can verify any individual sentence in a click -- confirm it against the source, catch a citation that doesn't hold up, or just read more context around the fact you asked about. It's the same underlying link between graph and source text that powers node detail pages in the graph view; chat citations are that mechanism surfaced at the point where you're actually reading an answer.&lt;/p&gt;

&lt;p&gt;In plain English: if Chaos Cypher tells you something, you can check it -- one click, straight to the sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Ask a question about a document you've already imported and look for the citation markers in the answer. If you haven't imported anything yet, the &lt;a href="https://chaoscypher.com/blog/graphrag-ollama-10-minutes" rel="noopener noreferrer"&gt;GraphRAG with Ollama in 10 minutes&lt;/a&gt; quickstart gets you from zero to a cited answer in about ten minutes, fully local.&lt;/p&gt;

</description>
      <category>graphrag</category>
      <category>ai</category>
      <category>rag</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>GraphRAG Teardown: What the Graph Actually Adds to Naive RAG</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:51:30 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/graphrag-teardown-what-the-graph-actually-adds-to-naive-rag-883</link>
      <guid>https://dev.to/chaoscypherinc/graphrag-teardown-what-the-graph-actually-adds-to-naive-rag-883</guid>
      <description>&lt;p&gt;Take three well-known AI agent papers -- chain-of-thought prompting, ReAct, and Reflexion -- drop them into a knowledge base, and ask a question that spans all three: &lt;em&gt;"How does Reflexion's self-reflection loop relate to chain-of-thought prompting?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Naive vector RAG will hand you a competent paragraph about chain-of-thought. The connection you actually asked about -- that ReAct interleaved chain-of-thought-style reasoning traces with actions, and that Reflexion layered verbal self-reflection on top of ReAct-style agents, with two authors carrying across both papers -- is spread across three documents and stated explicitly in none of them.&lt;/p&gt;

&lt;p&gt;This post is a teardown of both retrieval paths over exactly that corpus. Not a leaderboard: a walk through what each one mechanically does, where the vector path stalls, and where the graph earns its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Post Is (And Isn't)
&lt;/h2&gt;

&lt;p&gt;This is a &lt;strong&gt;mechanism teardown&lt;/strong&gt;, not a benchmark. You will not find a table here claiming Chaos Cypher scores X% against a baseline's Y%. Retrieval quality depends enormously on corpus, chunking, embedding model, and question shape, and a number produced by us on a corpus chosen by us would tell you very little.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;will&lt;/em&gt; find: the exact retrieval steps both paths take, the real relationships in this three-paper corpus that separate them, and the retrieval statistics Chaos Cypher emits on every query so you can run the same comparison on your own documents and read the receipts yourself.&lt;/p&gt;

&lt;p&gt;It's worth stating the honest version of the research picture up front. The most thorough public analysis of this question -- &lt;a href="https://arxiv.org/abs/2506.05690" rel="noopener noreferrer"&gt;&lt;em&gt;When to use Graphs in RAG: A Comprehensive Analysis for Graph Retrieval-Augmented Generation&lt;/em&gt;&lt;/a&gt; (the GraphRAG-Bench paper, ICLR 2026) -- exists precisely because graph retrieval &lt;strong&gt;frequently underperforms vanilla RAG&lt;/strong&gt; on many real-world tasks. Its contribution is mapping &lt;em&gt;when&lt;/em&gt; graph structure pays off: the benefit concentrates in complex, multi-hop reasoning, and largely evaporates on simple fact retrieval, where a well-tuned vector index is already the right tool.&lt;/p&gt;

&lt;p&gt;That matches our own position, and it's why Chaos Cypher fuses the two rather than replacing one with the other. The graph is not a better vector index. It answers a different question.&lt;/p&gt;

&lt;p&gt;In plain English: graphs help with questions that require connecting things, not with questions that require finding one thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Corpus and the Question
&lt;/h2&gt;

&lt;p&gt;Three papers, all publicly available:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Paper&lt;/th&gt;
&lt;th&gt;arXiv&lt;/th&gt;
&lt;th&gt;Venue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chain-of-Thought Prompting Elicits Reasoning in Large Language Models (Wei et al.)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2201.11903" rel="noopener noreferrer"&gt;2201.11903&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;NeurIPS 2022&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ReAct: Synergizing Reasoning and Acting in Language Models (Yao et al.)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2210.03629" rel="noopener noreferrer"&gt;2210.03629&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;ICLR 2023&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reflexion: Language Agents with Verbal Reinforcement Learning (Shinn et al.)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2303.11366" rel="noopener noreferrer"&gt;2303.11366&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;NeurIPS 2023&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The relationships between them are real and checkable. ReAct builds directly on chain-of-thought reasoning, interleaving reasoning traces with actions rather than producing reasoning alone. Reflexion builds on that line of work, adding a verbal self-reflection buffer that feeds an agent's own failures back into its next attempt. And the author lists overlap: Shunyu Yao and Karthik Narasimhan appear on both ReAct and Reflexion.&lt;/p&gt;

&lt;p&gt;Now the question: &lt;strong&gt;"How does Reflexion's self-reflection loop relate to chain-of-thought prompting?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Note what makes this hard. No single chunk in any of the three papers contains the answer. The chain runs chain-of-thought → ReAct → Reflexion, and each link lives in a different document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 1: What Naive Vector RAG Does
&lt;/h2&gt;

&lt;p&gt;The naive path is three steps, and it is genuinely good at what it does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Embed the question&lt;/strong&gt; into a vector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare&lt;/strong&gt; that vector against every chunk embedding by cosine similarity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return&lt;/strong&gt; the top &lt;em&gt;k&lt;/em&gt; closest chunks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ask our question and the retrieved chunks will be dominated by passages that &lt;em&gt;talk like the question&lt;/em&gt;. The phrase "self-reflection" pulls hard toward Reflexion's abstract and method section. "Chain-of-thought prompting" pulls toward the Wei et al. paper. You get strong chunks from both ends of the chain.&lt;/p&gt;

&lt;p&gt;What you almost certainly do not get is the middle. ReAct is the load-bearing link -- it's what connects reasoning traces to acting agents -- but a ReAct passage doesn't necessarily &lt;em&gt;sound&lt;/em&gt; like a question about Reflexion and chain-of-thought. Its vocabulary is its own: interleaving, action space, observation. Lexical and semantic similarity to the query is mediocre, so it ranks below chunks that merely restate the question's own terms.&lt;/p&gt;

&lt;p&gt;The failure mode is worth naming precisely: &lt;strong&gt;vector search has no representation of the fact that these three documents are related.&lt;/strong&gt; It sees text, scores text, returns text. Two chunks that describe the same idea in different vocabulary are far apart; two chunks that share vocabulary but no actual relationship are close together. There is no edge between ReAct and Reflexion in a vector index, because a vector index has no edges at all.&lt;/p&gt;

&lt;p&gt;And it fails quietly. There's no error, no "I found the endpoints but not the path." You get a fluent answer built from the two ends of a three-link chain, and it reads exactly as confident as a complete one would.&lt;/p&gt;

&lt;p&gt;In plain English: vector search finds text that sounds like your question, which is not the same as text that answers it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 2: What GraphRAG Does
&lt;/h2&gt;

&lt;p&gt;Chaos Cypher's GraphRAG pipeline runs seven steps. The graph half and the vector half both run, and the last step merges them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Embed the query.&lt;/strong&gt; Identical starting point to the naive path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Match seed entities.&lt;/strong&gt; Before touching document chunks, the query vector is compared against &lt;em&gt;entity&lt;/em&gt; embeddings in the knowledge graph. Entities clearing a cosine-similarity floor (&lt;code&gt;seed_similarity_threshold&lt;/code&gt;, default &lt;code&gt;0.3&lt;/code&gt;) become seeds, up to a &lt;code&gt;seed_limit&lt;/code&gt;. For our question, expect seeds like &lt;code&gt;Reflexion&lt;/code&gt;, &lt;code&gt;self-reflection&lt;/code&gt;, and &lt;code&gt;chain-of-thought prompting&lt;/code&gt; -- the anchor points, not the answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Personalized PageRank.&lt;/strong&gt; Standard PageRank finds globally important nodes. &lt;em&gt;Personalized&lt;/em&gt; PageRank restarts its random walk at your seeds, turning a global importance score into a query-specific relevance score. Chaos Cypher runs rustworkx's compiled power iteration with a damping factor of &lt;code&gt;0.85&lt;/code&gt; (&lt;code&gt;ppr_damping&lt;/code&gt;), in-process -- no external graph database. Seed weights come from the Step 2 similarity scores, so the walk is biased toward the entities that actually matter for &lt;em&gt;this&lt;/em&gt; question.&lt;/p&gt;

&lt;p&gt;This is where ReAct surfaces. It was never a strong seed, because it isn't semantically close to the query. It scores well because it sits structurally between two entities that &lt;em&gt;are&lt;/em&gt; -- one or two hops from both &lt;code&gt;Reflexion&lt;/code&gt; and &lt;code&gt;chain-of-thought&lt;/code&gt;. The top &lt;code&gt;ppr_top_k&lt;/code&gt; entities (default &lt;code&gt;20&lt;/code&gt;) carry forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Assemble graph context.&lt;/strong&gt; Seeds, discovered entities, and the relationship triples connecting them are collected into a structured context (capped by &lt;code&gt;max_triples&lt;/code&gt;, default &lt;code&gt;200&lt;/code&gt;) and passed to the model alongside the text. The model gets the &lt;em&gt;map&lt;/em&gt;, not just the territory. On this corpus, extraction should yield edges along the lines of &lt;code&gt;Reflexion --builds_on--&amp;gt; ReAct&lt;/code&gt;, &lt;code&gt;ReAct --extends--&amp;gt; chain-of-thought prompting&lt;/code&gt;, &lt;code&gt;Shunyu Yao --authored--&amp;gt; ReAct&lt;/code&gt;, &lt;code&gt;Shunyu Yao --authored--&amp;gt; Reflexion&lt;/code&gt; -- the exact labels depend on what extraction pulled from your documents, which is the subject of the limitations section below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Retrieve provenance chunks.&lt;/strong&gt; The first of two retrieval paths. For every entity the graph surfaced, Chaos Cypher looks up which document chunks that entity was originally extracted from. This is what makes the graph auditable rather than decorative: the ReAct passage arrives with the evidence for why it was retrieved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6 — Retrieve vector chunks.&lt;/strong&gt; The second path is ordinary hybrid search -- semantic plus keyword -- across all chunks. This is the naive path, running unchanged, inside the graph pipeline. It catches relevant passages that never produced graph entities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7 — Merge and rank.&lt;/strong&gt; Two independently ranked lists, two incompatible scoring scales. Chaos Cypher merges them with Reciprocal Rank Fusion (&lt;a href="https://dl.acm.org/doi/10.1145/1571941.1572114" rel="noopener noreferrer"&gt;Cormack, Clarke &amp;amp; Buettcher, 2009&lt;/a&gt;), which discards raw scores and uses only rank position: each chunk scores the sum of &lt;code&gt;1 / (k + rank)&lt;/code&gt; across every list it appears in, with &lt;code&gt;k = 60&lt;/code&gt; per the original paper.&lt;/p&gt;

&lt;p&gt;The property that matters: a chunk appearing in &lt;em&gt;both&lt;/em&gt; lists collects from both. A passage ranked 5th by provenance and 8th by vector search will often outrank one that's 1st by vector search and absent from provenance. Evidence confirmed by two independent signals beats evidence confirmed by one.&lt;/p&gt;

&lt;p&gt;In plain English: the graph finds the path between the things you asked about, the vector index finds text that sounds like your question, and fusion trusts what both agree on.&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%2Fyeha9i1bezyqe2x3nq8e.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%2Fyeha9i1bezyqe2x3nq8e.png" alt="Command palette searching for Pierre, listing entity results with connection counts" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Receipts
&lt;/h2&gt;

&lt;p&gt;The part that makes this a teardown rather than a story: every GraphRAG query returns a &lt;code&gt;retrieval_stats&lt;/code&gt; block alongside its results. Six fields, all observable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which pipeline actually ran (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;seed_entities_found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many graph entities anchored the walk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ppr_entities_explored&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many entities PageRank scored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;provenance_chunks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chunks retrieved via graph provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vector_chunks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chunks retrieved via hybrid search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deduplicated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chunks the two paths agreed on and were merged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;mode&lt;/code&gt; is the honest one. The pipeline degrades rather than failing, and it tells you which rung it landed on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;full_graphrag&lt;/code&gt;&lt;/strong&gt; -- seeds found, PageRank succeeded. Graph context, provenance chunks, vector chunks, fusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vector_only&lt;/code&gt;&lt;/strong&gt; -- embeddings worked, but no graph seeds matched (or PageRank produced nothing). Hybrid search, no graph context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;keyword_only&lt;/code&gt;&lt;/strong&gt; -- no embeddings available. SQLite full-text keyword search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the number to watch when you evaluate GraphRAG on your own corpus. A query returning &lt;code&gt;vector_only&lt;/code&gt; did not get any graph benefit, whatever the answer looked like -- and if most of your queries come back &lt;code&gt;vector_only&lt;/code&gt;, your graph isn't the problem, your &lt;em&gt;extraction&lt;/em&gt; is. Likewise, a &lt;code&gt;deduplicated&lt;/code&gt; count near zero means the two paths found completely disjoint evidence, which is usually a sign the query was single-hop and the graph half was along for the ride.&lt;/p&gt;

&lt;p&gt;In plain English: don't take our word for whether the graph fired -- the response tells you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Graph Doesn't Help
&lt;/h2&gt;

&lt;p&gt;Being straight about this is more useful than a win column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-hop fact lookup.&lt;/strong&gt; "What damping factor does PageRank use?" needs one chunk. The graph adds latency and nothing else. This is the GraphRAG-Bench finding in miniature, and it describes a lot of real queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small or unstructured corpora.&lt;/strong&gt; Three papers is enough to demonstrate a citation chain; a single document with no cross-references gives PageRank nothing to walk. Below a few interlinked documents, expect &lt;code&gt;vector_only&lt;/code&gt; most of the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extraction quality is the ceiling.&lt;/strong&gt; The graph is only as good as the entities and relationships pulled out of your text during import. Miss the &lt;code&gt;Reflexion --builds_on--&amp;gt; ReAct&lt;/code&gt; edge at extraction time and no amount of clever retrieval invents it. When GraphRAG disappoints, the fix is usually upstream in extraction, not in retrieval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Very large graphs skip PageRank entirely.&lt;/strong&gt; There's a &lt;code&gt;max_graph_nodes&lt;/code&gt; ceiling (default &lt;code&gt;50,000&lt;/code&gt;) on how much graph gets loaded for a walk. Past it, the pipeline skips PPR rather than stalling on a query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The seed threshold is a real gate.&lt;/strong&gt; Nothing clears &lt;code&gt;0.3&lt;/code&gt; cosine similarity, nothing seeds, and you're in &lt;code&gt;vector_only&lt;/code&gt; no matter how rich the graph is. Vocabulary mismatch between your question and your extracted entities shows up here first.&lt;/p&gt;

&lt;p&gt;In plain English: the graph is a specialist, and a system that pretends otherwise is selling something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run It Yourself
&lt;/h2&gt;

&lt;p&gt;Both halves are separately observable in a running instance, which is the point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The naive path alone:&lt;/strong&gt; the search bar exposes &lt;code&gt;keyword&lt;/code&gt;, &lt;code&gt;semantic&lt;/code&gt;, and &lt;code&gt;hybrid&lt;/code&gt; modes directly. &lt;code&gt;semantic&lt;/code&gt; is textbook vector RAG -- use it as your baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The full path:&lt;/strong&gt; ask the same question in &lt;strong&gt;Chat&lt;/strong&gt;, which retrieves through GraphRAG, and compare what gets cited. Every citation clicks through to the exact source chunk, so you can check whether the middle of the chain actually showed up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programmatically:&lt;/strong&gt; the &lt;code&gt;graphrag_search&lt;/code&gt; MCP tool runs the whole pipeline and returns &lt;code&gt;graph_context&lt;/code&gt;, &lt;code&gt;chunks&lt;/code&gt;, and &lt;code&gt;retrieval_stats&lt;/code&gt; as structured data -- the cleanest way to diff retrieval paths across a question set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The three papers above make a good starter corpus precisely because you already know the right answer, which makes a wrong one easy to spot.&lt;/p&gt;

&lt;p&gt;If you want the conceptual version of why multi-hop breaks vector search, that's &lt;a href="https://chaoscypher.com/blog/graphrag-enhanced-search" rel="noopener noreferrer"&gt;Why Your RAG Chat is Missing Half the Answers&lt;/a&gt;. If you don't have an instance running yet, &lt;a href="https://chaoscypher.com/blog/graphrag-ollama-10-minutes" rel="noopener noreferrer"&gt;GraphRAG with Ollama in 10 minutes&lt;/a&gt; gets you from zero to a cited answer, fully local.&lt;/p&gt;

</description>
      <category>graphrag</category>
      <category>rag</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>The CCX Open Format: A Portable Spec for Knowledge Graphs</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:50:48 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/the-ccx-open-format-a-portable-spec-for-knowledge-graphs-3p61</link>
      <guid>https://dev.to/chaoscypherinc/the-ccx-open-format-a-portable-spec-for-knowledge-graphs-3p61</guid>
      <description>&lt;p&gt;A knowledge graph you cannot take with you is not really yours. You can love the tool that built it, and still want the guarantee that the graph outlives the tool -- readable in five years, on a machine that has never heard of the software that produced it.&lt;/p&gt;

&lt;p&gt;That guarantee only exists if the file format is written down.&lt;/p&gt;

&lt;p&gt;So we wrote it down. CCX -- Chaos Cypher eXchange, the &lt;code&gt;.ccx&lt;/code&gt; file Chaos Cypher already exports -- now has a &lt;strong&gt;&lt;a href="https://chaoscypher.com/docs/reference/ccx-format" rel="noopener noreferrer"&gt;published draft specification&lt;/a&gt;&lt;/strong&gt; and a &lt;strong&gt;standalone reader library&lt;/strong&gt; you can install and use with no Chaos Cypher anywhere in the picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ccx-format        &lt;span class="c"&gt;# import name: ccx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reader is Apache-2.0 and lives in its own repository (&lt;a href="https://github.com/chaoscypherinc/ccx" rel="noopener noreferrer"&gt;chaoscypherinc/ccx&lt;/a&gt;, &lt;a href="https://pypi.org/project/ccx-format/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;) -- deliberately more permissive than Chaos Cypher itself, because a format nobody can implement against is a format nobody adopts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Changes For You
&lt;/h2&gt;

&lt;p&gt;If you use Chaos Cypher, nothing you do changes today. Export still works the way it did; the file you get is the same file.&lt;/p&gt;

&lt;p&gt;What changed is what that file &lt;em&gt;means&lt;/em&gt;. Before, &lt;code&gt;.ccx&lt;/code&gt; was an implementation detail -- whatever our exporter happened to write, readable by whatever our importer happened to accept. Now it is a documented artifact with a schema, a validator, and a second piece of software that reads it. Your export is no longer a bet on us staying in business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In plain English:&lt;/strong&gt; your &lt;code&gt;.ccx&lt;/code&gt; file is now a document with a spec behind it, not just a save file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually In a Package
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.ccx&lt;/code&gt; file is a ZIP archive with a fixed layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mimetype                              # first entry, uncompressed
manifest.json                         # metadata + member inventory
context.jsonld                        # the JSON-LD context, carried inline
knowledge.jsonld                      # the default graph
graphs/&amp;lt;namespace&amp;gt;.&amp;lt;name&amp;gt;.jsonld      # additional named graphs
sources.jsonl                         # source and chunk records
shapes.ttl                            # optional SHACL shapes
assets/sha256/&amp;lt;hex&amp;gt;                   # content-addressed assets
signatures/manifest.sig               # optional detached signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four decisions in there are worth pulling out, because they are the ones that make the file trustworthy rather than merely readable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The archive is deterministic.&lt;/strong&gt; Entry timestamps are pinned to &lt;code&gt;1980-01-01&lt;/code&gt; and file modes to &lt;code&gt;0644&lt;/code&gt;, so the same inputs produce a byte-identical archive. You can checksum an export and mean something by it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The media type is in the first bytes.&lt;/strong&gt; The first entry is always an uncompressed file named &lt;code&gt;mimetype&lt;/code&gt; containing &lt;code&gt;application/vnd.ccx+zip&lt;/code&gt; -- the same trick EPUB uses. A file identifies itself without being unpacked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything is checksummed twice.&lt;/strong&gt; Every graph and asset in the manifest carries both a SHA-256 and a SHA-512 digest, and integrity verification requires &lt;em&gt;both&lt;/em&gt; to match. Readers also enforce hard limits before inflating anything -- at most 100,000 entries, 512 MiB per entry, 2 GiB total -- so a malicious package cannot zip-bomb a consumer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote contexts are forbidden.&lt;/strong&gt; A conformant reader rejects any &lt;code&gt;http(s)://&lt;/code&gt; JSON-LD &lt;code&gt;@context&lt;/code&gt; reference anywhere in the package, and loads graphs with network access disabled. This is the rule we are most opinionated about: a package must be fully interpretable offline, forever, with no live server anywhere in the chain. It is a security property, not a style preference.&lt;/p&gt;

&lt;p&gt;The knowledge itself lives in &lt;code&gt;knowledge.jsonld&lt;/code&gt; as JSON-LD nodes, with relationships either attached directly to their subject or reified as standalone &lt;code&gt;Relationship&lt;/code&gt; resources when they carry properties of their own. Sources and retrieval chunks live in &lt;code&gt;sources.jsonl&lt;/code&gt;, one JSON object per line, with chunks pointing back into retained full text by character offset -- which is what lets a citation survive the trip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In plain English:&lt;/strong&gt; it's a ZIP with a strict layout, checksums on everything, and a hard rule that it never needs the internet to be read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading a Package Without Chaos Cypher
&lt;/h2&gt;

&lt;p&gt;This is the part that makes "open" mean something. Install the reader and open any &lt;code&gt;.ccx&lt;/code&gt; file:&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;ccx&lt;/span&gt;

&lt;span class="n"&gt;pkg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ccx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-graph.ccx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&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;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# e.g. True ('core', 'sources', 'shapes')
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a CLI too -- &lt;code&gt;ccx inspect&lt;/code&gt;, &lt;code&gt;ccx validate&lt;/code&gt;, and &lt;code&gt;ccx pack&lt;/code&gt; -- so you can check a package without writing any code at all.&lt;/p&gt;

&lt;p&gt;Note what &lt;code&gt;validate()&lt;/code&gt; hands back: not a boolean, but a report with errors, warnings, and a set of &lt;strong&gt;conformance classes&lt;/strong&gt;. Those classes are the format's honesty mechanism.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Granted when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The container, manifest, context, and graphs all check out.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sources&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sources.jsonl&lt;/code&gt; parses and every chunk offset is in range against its text asset.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shapes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;shapes.ttl&lt;/code&gt; is present, parses as Turtle, and declares at least one SHACL shape.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;embeddings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The manifest declares embeddings and every included sidecar is present.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;signed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The manifest declares signatures and at least one verifies offline.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The classes above &lt;code&gt;core&lt;/code&gt; are &lt;strong&gt;independent capabilities, not a stack&lt;/strong&gt;, and absence is never an error -- a package is judged only on what it claims by actually shipping the relevant artifacts. A package with no embeddings is not a failing package; it simply is not granted the &lt;code&gt;embeddings&lt;/code&gt; class. A package that claims embeddings and ships them broken does not fail validation either: it collects warnings and the class is withheld. So a consumer can ask "does this package have verifiable sources?" and get a straight answer, instead of parsing it and finding out.&lt;/p&gt;

&lt;p&gt;A typical full Chaos Cypher export validates as &lt;code&gt;core&lt;/code&gt;, &lt;code&gt;sources&lt;/code&gt;, &lt;code&gt;shapes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In plain English:&lt;/strong&gt; any Python program can open a &lt;code&gt;.ccx&lt;/code&gt; file, and the validator tells it exactly which parts of the package it can trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Producing One From Chaos Cypher
&lt;/h2&gt;

&lt;p&gt;Nothing new here, but for completeness -- the graphical path is &lt;strong&gt;Settings → General → Import &amp;amp; Export&lt;/strong&gt;: pick which components to include, click Export, and the browser downloads the &lt;code&gt;.ccx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tjk220eylo5g0zdhsbd.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%2F9tjk220eylo5g0zdhsbd.png" alt="Settings → General with the Import &amp;amp; Export row and its Export and Import buttons" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a single document's worth of knowledge rather than the whole graph, use &lt;strong&gt;Export Source&lt;/strong&gt; in that source's action menu on the Sources page.&lt;/p&gt;

&lt;p&gt;CLI users get the same thing with more control:&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="c"&gt;# Whole graph, default components&lt;/span&gt;
chaoscypher graph package &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; my-research.ccx &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"My Research"&lt;/span&gt;

&lt;span class="c"&gt;# Templates and lenses only, no entities -- a shareable schema&lt;/span&gt;
chaoscypher graph package &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--no-knowledge&lt;/span&gt; &lt;span class="nt"&gt;--no-workflows&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; schema-only.ccx

&lt;span class="c"&gt;# Include embedding vectors (off by default -- only useful for same-model migration)&lt;/span&gt;
chaoscypher graph package &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--embeddings&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; full.ccx

&lt;span class="c"&gt;# Load one back&lt;/span&gt;
chaoscypher graph package load my-research.ccx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imports are an &lt;strong&gt;upsert by IRI&lt;/strong&gt;, not a copy: Chaos Cypher mints locally scoped identifiers and preserves foreign ones on re-export, so loading a package twice does not duplicate its contents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In plain English:&lt;/strong&gt; two clicks in Settings, or one command in the terminal, and you have a package.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Limits
&lt;/h2&gt;

&lt;p&gt;This is draft 1 of a 3.0 spec, and there is a difference between "documented" and "finished." The things we would want to know if we were reading someone else's format announcement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;We are not calling it a standard.&lt;/strong&gt; There is one reference implementation, written by us. A format with a single implementation is a documented format, and that is all we are claiming. That word is available to us when a second, independent implementation exists -- not before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spec is revised in place&lt;/strong&gt; while draft status holds. The revision marker at the top of the spec page identifies the exact text a tool was written against; check it rather than assuming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflows do not round-trip today.&lt;/strong&gt; The &lt;code&gt;chaoscypher.workflows&lt;/code&gt; graph member currently carries workflow &lt;em&gt;trigger&lt;/em&gt; rows only, not full workflow definitions, and our own importer skips that member with a warning. It is in the package and it is not yet useful; we would rather say so than let you discover it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signatures are specified but not emitted.&lt;/strong&gt; The format defines detached &lt;code&gt;ed25519&lt;/code&gt; and &lt;code&gt;sigstore&lt;/code&gt; signatures with offline, fail-closed verification, and reserves post-quantum format names. Chaos Cypher does not produce signed packages yet, so the &lt;code&gt;signed&lt;/code&gt; class is currently unreachable from our exports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings are opt-in and model-bound.&lt;/strong&gt; They ship as Parquet sidecars and are off by default, because a vector is only useful to someone running the same embedding model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In plain English:&lt;/strong&gt; the file format is written down and stable enough to build against; the parts that are not finished are named above rather than left for you to trip over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Want From This
&lt;/h2&gt;

&lt;p&gt;The interesting question is not whether Chaos Cypher can read its own files. It is whether anything else can.&lt;/p&gt;

&lt;p&gt;So the specific thing that would make this real: &lt;strong&gt;one independent implementation.&lt;/strong&gt; A loader that pulls a &lt;code&gt;.ccx&lt;/code&gt; into LlamaIndex or LangChain. An exporter that writes one out of Obsidian. A validator in another language. Any of those, written by someone who is not us, is the difference between a documented format and a shared one.&lt;/p&gt;

&lt;p&gt;If you are building in this space, the &lt;a href="https://chaoscypher.com/docs/reference/ccx-format" rel="noopener noreferrer"&gt;spec page&lt;/a&gt; is the whole contract and the &lt;a href="https://github.com/chaoscypherinc/ccx" rel="noopener noreferrer"&gt;reader&lt;/a&gt; is the reference. Both are open, and issues and questions on either are genuinely welcome -- draft 1 is exactly the stage where the spec should be argued with.&lt;/p&gt;

&lt;p&gt;If you just want your own graph to be portable: it already is. That was the point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running Chaos Cypher fully locally with Ollama? See the &lt;a href="https://chaoscypher.com/blog/local-ai-knowledge-graph" rel="noopener noreferrer"&gt;local-first setup guide&lt;/a&gt; -- packages work identically there, and never touch a network.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>graphrag</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>ChaosCypher v0.4.3: When a Clean Restart Lost More Work Than a Crash</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Tue, 22 Sep 2026 20:35:42 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/chaoscypher-v043-when-a-clean-restart-lost-more-work-than-a-crash-1c2a</link>
      <guid>https://dev.to/chaoscypherinc/chaoscypher-v043-when-a-clean-restart-lost-more-work-than-a-crash-1c2a</guid>
      <description>&lt;p&gt;v0.4.3 adds nothing new. No new features, no breaking API changes, no schema migrations: 54 commits since v0.4.2, 23 of them changing shipped code. Most of them are in the queue and the extraction pipeline, and most of those share one trait. The work was lost, counted twice, or quietly downgraded, and nothing reported it.&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%2F02uolct3m935zqakcdhn.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%2F02uolct3m935zqakcdhn.png" alt="Knowledge graph visualization showing extracted entities and relationships" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A clean restart was worse than &lt;code&gt;kill -9&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stopping a worker gracefully destroyed work that a hard kill would have recovered.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On shutdown, the drain's &lt;code&gt;CancelledError&lt;/code&gt; arm wrote terminal &lt;code&gt;status="cancelled"&lt;/code&gt; to the tasks it interrupted. That is exactly the status the requeue script and &lt;code&gt;retry_task&lt;/code&gt; refuse to resurrect. So the recovery contract ran backwards: a &lt;code&gt;SIGKILL&lt;/code&gt; left the task recoverable, and a polite restart finished it off.&lt;/p&gt;

&lt;p&gt;It was one of four queue-recovery defects in this release that each looked like a working system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crash recovery spent two units of the retry budget per cycle.&lt;/strong&gt; &lt;code&gt;requeue_atomic.lua&lt;/code&gt; bumped &lt;code&gt;attempts&lt;/code&gt; on top of the worker's own claim-time increment, so a &lt;code&gt;max_tries=5&lt;/code&gt; task with &lt;code&gt;retry_on_crash=True&lt;/code&gt; got 3 dispatches, not 5, and then went terminal reporting "crashed after 5 attempts". The affected operations are the four crash-retryable ones with no owning resource: &lt;code&gt;execute_workflow&lt;/code&gt;, &lt;code&gt;execute_step&lt;/code&gt;, &lt;code&gt;export_graph&lt;/code&gt;, &lt;code&gt;export_by_sources&lt;/code&gt;. For those, the queue budget is the only recovery layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A lapsed heartbeat could never be refreshed.&lt;/strong&gt; &lt;code&gt;refresh_heartbeat&lt;/code&gt; issued an &lt;code&gt;EXPIRE&lt;/code&gt;, which is a silent no-op on a key that has already expired. A 31-second stall condemned a multi-hour task as abandoned, and the reconciler requeued it for duplicate execution. It is now a &lt;code&gt;SET&lt;/code&gt; with &lt;code&gt;EX&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A re-delivered chunk task could finish the job one chunk early.&lt;/strong&gt; The short-circuit that made re-running an extraction chunk safe only caught a &lt;code&gt;completed&lt;/code&gt; row. A row still in &lt;code&gt;running&lt;/code&gt; went straight through, so two deliveries produced two terminal writes and two job-counter bumps. Both transitions are now guarded updates that return a rowcount, and the handler bails out &lt;em&gt;before&lt;/em&gt; the billable LLM call when it loses the claim.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In plain English: if you restart workers, or your machine stalls for half a minute, queued work now comes back the number of times it was promised to, and only once.&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%2F280awt2hmpkswiersmbh.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%2F280awt2hmpkswiersmbh.png" alt="Queue monitor showing task execution status" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Graph edges that explained themselves by thinking out loud
&lt;/h3&gt;

&lt;p&gt;Every relationship ChaosCypher extracts carries a justification: the evidence the model gave for linking two entities. You can inspect it, and it is part of why the graph is checkable rather than taken on trust. That only works if the justification is evidence.&lt;/p&gt;

&lt;p&gt;Some weren't. One &lt;code&gt;parent_of&lt;/code&gt; edge at confidence 0.9 explained itself with &lt;em&gt;"I will link 9 to 3 via &lt;code&gt;interacts_with&lt;/code&gt;"&lt;/em&gt;. That is the model deliberating, stored as if it were the reason the edge exists. Justifications that carry reasoning markers are now blanked, and the rest are trimmed to two sentences on a sentence or word boundary, so no fragment survives.&lt;/p&gt;

&lt;p&gt;In plain English: when you read why an edge exists, you now get a reason or nothing, not the model's scratchpad.&lt;/p&gt;

&lt;h3&gt;
  
  
  One MCP sandbox bypass, and a read-only mode that wasn't
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;MCP &lt;code&gt;add_document&lt;/code&gt; skipped its sandbox containment whenever &lt;code&gt;content&lt;/code&gt; was supplied.&lt;/strong&gt; The tool schema says that supplying content means the file is never loaded. Nothing implemented that: &lt;code&gt;file_path&lt;/code&gt; was read from disk on every path and reached the loader uncontained. A caller supplying both got an arbitrary-file read that reached &lt;code&gt;/data/credentials.json&lt;/code&gt;. The containment and dotfile guards are now unconditional, in the server and in the processor's mirror of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;chaoscypher mcp&lt;/code&gt; honoured a read-only install only when you typed the flag.&lt;/strong&gt; The guard read the &lt;code&gt;--mode&lt;/code&gt; flag rather than the effective setting, so the documented Claude Desktop invocation against a &lt;code&gt;read&lt;/code&gt;-configured install still handed the external client the destructive &lt;code&gt;apply_upgrade&lt;/code&gt; tool. The guard now reads the configured mode, and an explicit &lt;code&gt;--mode write&lt;/code&gt; still overrides it.&lt;/p&gt;

&lt;p&gt;Also closed: the API-key selector and bearer-token throttle defects found in a post-merge sweep; &lt;code&gt;decode_session&lt;/code&gt; rejects non-ASCII input; &lt;code&gt;summarize&lt;/code&gt; gets the untrusted-document fence its five sibling handlers already had; and &lt;code&gt;CHAOSCYPHER_ALLOW_USER_PLUGINS=0&lt;/code&gt;, the documented Docker kill switch for user plugins, now actually reaches all five plugin-loading services. Before, no service declared it, so the loader always saw its default.&lt;/p&gt;

&lt;p&gt;In plain English: if you point an AI assistant at ChaosCypher over MCP, it can no longer read files outside the sandbox, and "read-only" means read-only however you launch it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Settings that silently did nothing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every configured trigger filter matched everything.&lt;/strong&gt; &lt;code&gt;list_triggers&lt;/code&gt; deferred the &lt;code&gt;filters&lt;/code&gt; and &lt;code&gt;actions&lt;/code&gt; columns the dispatcher reads, so a trigger scoped to one source fired on every source. The &lt;code&gt;.ccx&lt;/code&gt; exporter and &lt;code&gt;GET /workflows/{id}/triggers&lt;/code&gt; were losing the same keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rejected &lt;code&gt;workers.yaml&lt;/code&gt; value collapsed the Operations queue from 8 concurrent slots to 1&lt;/strong&gt;, because a bad value fell through to a boolean instead of the worker type's default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The commit-time orphan filter ignored the source's own settings&lt;/strong&gt; and always dropped orphans, even for minimal or unfiltered sources and rows with &lt;code&gt;protect_orphans=True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The queue reset promised cancellation and delivered dead letters.&lt;/strong&gt; It now runs &lt;code&gt;cancel_all_tasks&lt;/code&gt; first, so running tasks stop instead of landing in the dead-letter set.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The API stops freezing on Cancel All
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;POST /queue/cancel-all&lt;/code&gt; ran a synchronous connect → SELECT → commit → disconnect on the event loop, once per task. After a large import that is thousands of cycles inside one request with no awaits, and Cortex served nothing else in the meantime. All four call sites now go through a worker thread. The same treatment went to per-chunk progress writes on the worker. A commit that lost the SQLite writer lock could park the loop for up to 60 seconds and starve the heartbeat refresher, which is how live tasks started to look abandoned.&lt;/p&gt;

&lt;p&gt;The vision-page fan-out went from up to 4000 sequential round trips (bounded by &lt;code&gt;vision_max_pages&lt;/code&gt;, default 2000) to 2. The changelog has the rest of the round: projections and unprojected reads fixed across the stuck-source sweep, step progress, queue stats and &lt;code&gt;chaoscypher source search&lt;/code&gt;. These describe what the code was doing, not a benchmark.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading
&lt;/h3&gt;

&lt;p&gt;No migrations apply. Drain the queue before you swap the image: stop new submissions and wait for &lt;code&gt;/api/v1/queue/stats&lt;/code&gt; to report 0 pending on all queues. Don't run mixed versions against the same queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/chaoscypherinc/chaoscypher:0.4.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you run the Python packages directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; chaoscypher-core chaoscypher-cortex chaoscypher-neuron chaoscypher-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting fresh:&lt;br&gt;
&lt;/p&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;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; chaoscypher &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 443:443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; chaoscypher-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host.docker.internal:host-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/chaoscypherinc/chaoscypher:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Next steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;If you use MCP with a &lt;code&gt;read&lt;/code&gt;-configured install, upgrade first. That is the fix with the widest reach.&lt;/li&gt;
&lt;li&gt;If you set trigger filters, check your automations after upgrading. Triggers you scoped to one source now fire only on that source.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full details in the &lt;a href="https://chaoscypher.com/docs/about/changelog" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;. ChaosCypher is AGPL-3.0 and local-first: the graph, the chat, and the import and export paths all run on your own machine. Repo: &lt;a href="https://github.com/chaoscypherinc/chaoscypher" rel="noopener noreferrer"&gt;https://github.com/chaoscypherinc/chaoscypher&lt;/a&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>python</category>
      <category>rag</category>
    </item>
    <item>
      <title>Automate Your Knowledge Pipeline: Triggers, Workflows, and AI Tools</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:46:49 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/automate-your-knowledge-pipeline-triggers-workflows-and-ai-tools-424g</link>
      <guid>https://dev.to/chaoscypherinc/automate-your-knowledge-pipeline-triggers-workflows-and-ai-tools-424g</guid>
      <description>&lt;p&gt;Most knowledge management tools treat you like a filing clerk. Upload a document, wait for extraction, manually review entities, fix errors, tag things, connect things. Then do it all again for the next document. And the next. And the next fifty.&lt;/p&gt;

&lt;p&gt;This is fine when you have ten documents. It falls apart at a hundred. It becomes genuinely painful at a thousand. The bottleneck is never the AI -- it's the human loop. Every document requires your attention, your judgment calls, your clicks. The extraction might take thirty seconds. Your review and cleanup take ten minutes.&lt;/p&gt;

&lt;p&gt;Chaos Cypher's workflow engine exists to close that gap. You define a processing pipeline once -- what to extract, how to validate it, where to send notifications -- and every new document flows through it automatically. No babysitting. No repetitive clicking. You set the rules, the system follows them.&lt;/p&gt;

&lt;p&gt;This isn't a cron job bolted onto the side. It's a proper workflow engine with event-driven triggers, conditional branching, step-to-step data passing, and composable AI tools. Let me walk you through how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Concrete Workflow: Auto-Processing Research Papers
&lt;/h2&gt;

&lt;p&gt;Abstractions are boring. Let's look at a real workflow you might build: automatically processing research papers as they're uploaded.&lt;/p&gt;

&lt;p&gt;Here's the pipeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger:&lt;/strong&gt; Extraction commits new entities to the graph (&lt;code&gt;node.create&lt;/code&gt; event fires) -- the signal that a freshly uploaded paper has finished processing. (You can also invoke the workflow manually or through the API.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 -- AI Prompt:&lt;/strong&gt; Summarize the document in three sentences. The &lt;code&gt;ai.prompt&lt;/code&gt; tool sends the document text to your configured LLM with instructions to produce a concise summary. If the document is long, it automatically chunks the text and processes sections in parallel, then merges the results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 -- AI Extract JSON:&lt;/strong&gt; Pull out structured metadata. Authors, publication date, journal name, key findings, methodology type. The &lt;code&gt;ai.extract_json&lt;/code&gt; tool takes the document text and a JSON schema defining exactly what you want, then returns validated structured data. It retries if the extraction doesn't match the schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 -- Conditional:&lt;/strong&gt; Check if this is a clinical study. The &lt;code&gt;logic.conditional&lt;/code&gt; tool evaluates whether &lt;code&gt;{{steps.extract_metadata.extracted_data.methodology_type}}&lt;/code&gt; equals &lt;code&gt;"clinical_trial"&lt;/code&gt;. If true, the workflow branches to run additional &lt;a href="https://chaoscypher.com/blog/domain-extraction-guide" rel="noopener noreferrer"&gt;medical-domain extraction&lt;/a&gt;. If false, it skips ahead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 -- HTTP Request:&lt;/strong&gt; Post a notification to a Slack webhook with the summary from Step 1 and the metadata from Step 2. The &lt;code&gt;http.request&lt;/code&gt; tool sends a POST to your webhook URL with a JSON body containing &lt;code&gt;{{steps.summarize.result}}&lt;/code&gt; and &lt;code&gt;{{steps.extract_metadata.extracted_data}}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Notice the &lt;code&gt;{{steps.summarize.result}}&lt;/code&gt; syntax. That's the interpolation engine at work. Every step's output is keyed by the step's ID -- reference it as &lt;code&gt;{{steps.&amp;lt;step_id&amp;gt;.&amp;lt;field&amp;gt;}}&lt;/code&gt; from any later step. The IDs here (&lt;code&gt;summarize&lt;/code&gt;, &lt;code&gt;extract_metadata&lt;/code&gt;) are placeholders: IDs are instance-specific, so check a step's actual ID in the builder (or via &lt;code&gt;GET /api/v1/workflows/{id}&lt;/code&gt;) before wiring references -- and re-check after importing a workflow, because the export format does not carry step IDs, so import assigns fresh ones and cross-step references in configurations must be updated. You can reference &lt;code&gt;{{inputs.&amp;lt;field&amp;gt;}}&lt;/code&gt; for the data the workflow was invoked with, &lt;code&gt;{{steps.extract_metadata.extracted_data.authors}}&lt;/code&gt; for a nested field from a previous step, or even &lt;code&gt;{{steps.check_clinical.branch_taken}}&lt;/code&gt; to see which conditional path was followed. The interpolation preserves types too -- if a previous step returned a number, you get a number, not the string &lt;code&gt;"42"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This entire pipeline runs without human intervention. Upload a PDF, walk away, come back to a summarized, metadata-tagged, conditionally-processed document with a Slack notification waiting for you.&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%2F280awt2hmpkswiersmbh.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%2F280awt2hmpkswiersmbh.png" alt="Queue monitor showing task status and history" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood: 10 Built-In Tools
&lt;/h2&gt;

&lt;p&gt;The workflow engine ships with ten built-in tools organized into five categories. Each tool has a defined input schema and output schema, so the system validates your configuration before anything runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;What They Do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ai.prompt&lt;/code&gt;, &lt;code&gt;ai.extract_json&lt;/code&gt;, &lt;code&gt;ai.vector_search&lt;/code&gt;, &lt;code&gt;ai.generate_embedding&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;LLM interactions with chunking support, structured JSON extraction with schema validation and retries, semantic search across your knowledge graph, vector embedding generation for entities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;data.extract&lt;/code&gt;, &lt;code&gt;data.merge&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Pull values from nested objects using dot-notation paths (&lt;code&gt;user.addresses.0.city&lt;/code&gt;), merge multiple dictionaries with shallow or deep strategies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;logic.conditional&lt;/code&gt;, &lt;code&gt;logic.loop&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;If/then branching with safe expression evaluation, iterate over collections with configurable limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http.request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;External API calls with all HTTP methods, bearer/basic auth, configurable timeouts, and SSRF protection that blocks localhost access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Templates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;templates.list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Query your knowledge graph schema to discover available node templates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three details that make these reliable enough to run unattended: &lt;code&gt;ai.prompt&lt;/code&gt; automatically chunks documents that exceed the model's context window, processes the chunks in parallel, and merges the results; &lt;code&gt;ai.extract_json&lt;/code&gt; validates the LLM's output against your JSON schema and retries on mismatch; and &lt;code&gt;http.request&lt;/code&gt; validates URLs before sending (https/http only, localhost blocked against SSRF) while supporting all standard methods and auth schemes. &lt;code&gt;ai.vector_search&lt;/code&gt; is the interesting one for graph-aware pipelines -- workflows can semantically query existing knowledge, enabling steps like "find entities similar to what we just extracted and check for duplicates."&lt;/p&gt;

&lt;h3&gt;
  
  
  How Triggers Work
&lt;/h3&gt;

&lt;p&gt;Triggers are the entry point for automated workflows. They listen for events in the system and fire workflows when conditions are met.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event sources&lt;/strong&gt; define what happened. Two are live today: &lt;code&gt;node.create&lt;/code&gt; (a new node was committed to the graph) and &lt;code&gt;edge.create&lt;/code&gt; (a new relationship was committed) -- both fire on the extraction-commit path. Other event sources, like file-upload and import-complete, are selectable in the builder but not yet emitted by the engine. The system ships with a built-in auto-embedding trigger -- nodes created when an extraction commits to the graph automatically get vector embeddings generated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filters&lt;/strong&gt; let you narrow the scope, using exact key-value matching against the event data. A trigger on &lt;code&gt;node.create&lt;/code&gt; with a filter &lt;code&gt;{"entity_type": "node"}&lt;/code&gt; fires only for node events. The &lt;code&gt;node.create&lt;/code&gt; payload currently carries &lt;code&gt;entity_type&lt;/code&gt; and &lt;code&gt;entity_id&lt;/code&gt;, so filters on other keys (like a template id) can never match -- the engine logs a warning when a trigger is wired to a structurally impossible filter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statistics tracking&lt;/strong&gt; gives you visibility. Every trigger execution records success/failure status, execution time, and error messages. You can see your success rate, average execution time, and recent execution history -- useful for debugging workflows that occasionally fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Expose as AI Tool" Feature
&lt;/h3&gt;

&lt;p&gt;Here's where things get composable. Any workflow can be exposed as a callable AI tool by setting &lt;code&gt;expose_as_ai_tool: true&lt;/code&gt; and defining input/output schemas. Once exposed, that workflow appears alongside the built-in tools and can be used as a step in other workflows.&lt;/p&gt;

&lt;p&gt;Think about what this enables. You build a workflow that extracts and validates medical terminology. You expose it as a tool. Now your "process research papers" workflow can call it as Step 3 instead of hardcoding medical-domain logic. You have a workflow that enriches person entities by cross-referencing external APIs? Expose it, and any other workflow can use it.&lt;/p&gt;

&lt;p&gt;Workflows calling workflows. Each one focused on a single job, composed together into pipelines of arbitrary complexity. The step type &lt;code&gt;workflow&lt;/code&gt; (alongside &lt;code&gt;system_tool&lt;/code&gt; and &lt;code&gt;user_tool&lt;/code&gt;) tells the engine to execute another workflow as a step, passing inputs and receiving outputs just like any other tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow Portability
&lt;/h3&gt;

&lt;p&gt;Workflows export to a version-stamped, self-describing JSON file -- the definition, all steps, all configurations, no hidden state, no implicit database IDs. Import validates the version and resolves every referenced tool against the target instance's registry before creating anything, so a missing tool produces a clear error instead of a broken workflow. Export from your laptop, import on a server, share with a colleague.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The fastest way to see the workflow engine in action is to look at the export format. Here's a minimal workflow that summarizes documents on upload:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workflow"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Summarize on Upload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Auto-summarize new documents when uploaded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_schema"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"document_text"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The document content to summarize"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required"&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;"document_text"&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;"output_schema"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"summary"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Three-point summary of the document"&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="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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"steps"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"step_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Summarize Document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system_tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_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;"ai.prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"configuration"&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;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Summarize this document in 3 key points:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;{{inputs.document_text}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"output_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&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="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="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;This is everything the system needs. The &lt;code&gt;version&lt;/code&gt; field ensures forward compatibility. The &lt;code&gt;input_schema&lt;/code&gt; and &lt;code&gt;output_schema&lt;/code&gt; define the contract. The &lt;code&gt;steps&lt;/code&gt; array contains the pipeline.&lt;/p&gt;

&lt;p&gt;Each step specifies its &lt;code&gt;tool_type&lt;/code&gt; (&lt;code&gt;system_tool&lt;/code&gt;, &lt;code&gt;user_tool&lt;/code&gt;, or &lt;code&gt;workflow&lt;/code&gt;), a &lt;code&gt;tool_id&lt;/code&gt; that references a registered tool, and a &lt;code&gt;configuration&lt;/code&gt; object whose shape matches the tool's input schema. The &lt;code&gt;{{inputs.document_text}}&lt;/code&gt; template variable gets resolved at execution time with the inputs the workflow was invoked with.&lt;/p&gt;

&lt;p&gt;To wire it up, create a trigger with the event source (like &lt;code&gt;node.create&lt;/code&gt;), link it to your workflow, and optionally add filters. From there the engine scales with you: steps can declare &lt;code&gt;depends_on&lt;/code&gt;, &lt;code&gt;continue_on_error&lt;/code&gt;, &lt;code&gt;max_retries&lt;/code&gt; (per-step, falling back to the workflow-level default), and &lt;code&gt;timeout_seconds&lt;/code&gt;; imports handle name conflicts (&lt;code&gt;fail&lt;/code&gt;/&lt;code&gt;skip&lt;/code&gt;/&lt;code&gt;rename&lt;/code&gt;) and can land inactive for a test run first. The &lt;a href="https://chaoscypher.com/docs/reference/api/workflows" rel="noopener noreferrer"&gt;workflows API reference&lt;/a&gt; documents the step fields and import options.&lt;/p&gt;

&lt;p&gt;One more thing worth knowing before you trust a pipeline to run unattended: every run produces an execution record -- status, inputs, outputs, per-step timing, and which step failed and why. When a workflow fails at 3am, you don't guess. You look at the execution detail, see that Step 3 timed out after 120 seconds waiting for the LLM, and adjust accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The workflow engine is designed to grow. The tool system uses a plugin architecture -- the same pattern that powers Chaos Cypher's loader plugins, domain plugins, and LLM providers. Custom tool plugins in Python shipped: drop a &lt;code&gt;*_plugin.py&lt;/code&gt; file in &lt;code&gt;data/plugins/tools/&lt;/code&gt; implementing &lt;code&gt;tool_id&lt;/code&gt;, &lt;code&gt;category&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;input_schema&lt;/code&gt;, and an &lt;code&gt;execute&lt;/code&gt; method, and it auto-registers on startup (user plugins even override built-ins with the same &lt;code&gt;tool_id&lt;/code&gt;). See the &lt;a href="https://chaoscypher.com/docs/user-guide/tool-plugins" rel="noopener noreferrer"&gt;tool plugins guide&lt;/a&gt; for the full interface.&lt;/p&gt;

&lt;p&gt;More trigger event sources are coming as the platform grows. Scheduling (run a workflow every Tuesday at 9am) and webhook triggers (fire a workflow from an external system) are natural extensions of the existing event-driven architecture.&lt;/p&gt;

&lt;p&gt;If you've built an interesting automation workflow -- whether it's a multi-step research pipeline, a quality assurance checker, or an integration with external tools -- I'd genuinely like to hear about it. The export format makes sharing straightforward: export your workflow, share the JSON, and someone else can import it and adapt it to their use case. That's the whole point of portability.&lt;/p&gt;

&lt;p&gt;For the full API reference and detailed configuration options, check out the &lt;a href="https://chaoscypher.com/docs/reference/api/workflows" rel="noopener noreferrer"&gt;workflow documentation&lt;/a&gt;. The built-in system workflows (like auto-embedding on node create) are also good starting points -- export them and study the step configurations to see how the engine's own automation is wired together.&lt;/p&gt;

</description>
      <category>workflows</category>
      <category>automation</category>
      <category>ai</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>From Obsidian Vault to AI-Powered Knowledge Graph in Minutes</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:46:28 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/from-obsidian-vault-to-ai-powered-knowledge-graph-in-minutes-2dm1</link>
      <guid>https://dev.to/chaoscypherinc/from-obsidian-vault-to-ai-powered-knowledge-graph-in-minutes-2dm1</guid>
      <description>&lt;p&gt;Open your Obsidian graph view and look at it honestly: every line on that screen is a link you typed by hand. The person mentioned in forty notes but never &lt;code&gt;[[linked]]&lt;/code&gt;, the concept that appears under three different names, the two projects connected by a decision you wrote down once and forgot -- none of it shows up. Your vault knows more than its graph does.&lt;/p&gt;

&lt;p&gt;Chaos Cypher adds the missing layer. It reads the Markdown itself, extracts entities and relationships with an LLM, and makes the whole vault searchable and chat-ready -- without requiring a single connection to be pre-linked. Your manual wiki-links stay exactly what they are: deliberate structure. The AI-discovered layer sits alongside them, surfacing the connections you never typed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow
&lt;/h2&gt;

&lt;p&gt;No plugin required -- the archive loader does the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zip the notes you want to analyze.&lt;/strong&gt; Start with a focused folder of at least ~10 notes rather than the entire vault, and leave out private notes you don't want processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload the ZIP (or TAR.GZ) as a source.&lt;/strong&gt; The archive loader recognizes Markdown-heavy projects, strips frontmatter, preserves heading structure, and sends each note through the same pipeline used for PDFs and web pages. Smaller archives are processed by the generic loader, which keeps frontmatter intact -- bundle at least ten notes to engage the Markdown handler's frontmatter stripping.&lt;/li&gt;
&lt;/ol&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%2Ff8zz473pp5qlkhncvd8m.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%2Ff8zz473pp5qlkhncvd8m.png" alt="Add Source dialog with URL input and file drag-and-drop" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confirm the detected domain.&lt;/strong&gt; Indexing (chunking + local embeddings) takes seconds and immediately enables semantic search and RAG chat. After indexing, Chaos Cypher analyzes your notes and proposes an &lt;a href="https://chaoscypher.com/docs/user-guide/domains" rel="noopener noreferrer"&gt;extraction domain&lt;/a&gt; -- review and confirm (or override) it in the dialog, and extraction starts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wait for extraction.&lt;/strong&gt; Extraction builds the graph: typed entities and relationships pulled from the prose itself.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explore what the AI found.&lt;/strong&gt; People mentioned across multiple notes, concepts that appear under different headings, projects connected by shared decisions, recurring themes that were never explicitly linked -- now visible, navigable, and traceable back to the exact notes they came from.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2F02uolct3m935zqakcdhn.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%2F02uolct3m935zqakcdhn.png" alt="Knowledge graph visualization showing extracted entities and relationships" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask questions.&lt;/strong&gt; "What decisions led to the current architecture?" or "Where do my notes on attention mechanisms and retrieval overlap?" -- answered with citations back to your own notes, not the internet.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Scripting the Import
&lt;/h2&gt;

&lt;p&gt;The steps above walk through the UI, but the same pipeline is reachable without opening a browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For developers:&lt;/strong&gt; &lt;code&gt;POST /api/v1/sources&lt;/code&gt; takes a multipart file upload and runs it through the identical loader dispatch the UI uses -- point it at your vault archive and it works the same way. &lt;code&gt;POST /api/v1/sources/batch&lt;/code&gt; accepts a list of files if you're importing several exports at once, which is handy wired into a nightly vault-backup job: export, POST the archive, let indexing and extraction pick it up. In plain English: anything the "Upload" button does, a script can do too, over HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For CLI users:&lt;/strong&gt; the same import is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chaoscypher &lt;span class="nb"&gt;source &lt;/span&gt;add my-vault.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;source add&lt;/code&gt; isn't Obsidian-specific -- it's the general-purpose source-loading command, and it routes to the archive loader the same way the web UI does whenever it sees a &lt;code&gt;.zip&lt;/code&gt; or &lt;code&gt;.tar.gz&lt;/code&gt;. There's no separate "import vault" subcommand, because none is needed. In plain English: if you'd rather stay in the terminal, one command gets your vault into the graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Few Tips for a Good First Pass
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep filenames and headings descriptive -- they become context the extractor uses.&lt;/li&gt;
&lt;li&gt;If your vault has a specialized vocabulary, review the extracted graph and then tune &lt;a href="https://chaoscypher.com/docs/user-guide/domains" rel="noopener noreferrer"&gt;domains or templates&lt;/a&gt; to match it.&lt;/li&gt;
&lt;li&gt;Everything runs locally if you pair it with &lt;a href="https://chaoscypher.com/blog/local-ai-knowledge-graph" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; -- your journal never leaves your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This workflow shines wherever the value isn't only in the notes but in the relationships between them: research notes, project journals, meeting archives, long-running personal knowledge bases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A first-class Obsidian importer -- reading vaults directly, preserving wiki-links as graph edges alongside the AI-discovered ones -- is on the roadmap. If that's your use case, &lt;a href="https://github.com/chaoscypherinc/chaoscypher/discussions" rel="noopener noreferrer"&gt;tell us how you'd want it to work&lt;/a&gt;; vault structures vary wildly and real examples shape the design.&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>graphrag</category>
      <category>selfhosted</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Extract Smarter: How Domain-Aware AI Builds Better Knowledge Graphs</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:47:08 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/extract-smarter-how-domain-aware-ai-builds-better-knowledge-graphs-252d</link>
      <guid>https://dev.to/chaoscypherinc/extract-smarter-how-domain-aware-ai-builds-better-knowledge-graphs-252d</guid>
      <description>&lt;p&gt;Most AI extraction tools treat every document the same way. Upload a medical paper or a legal contract and you get the same generic entity types, the same vague relationships, the same disappointing graph. Chaos Cypher takes a different approach: it detects what kind of document you uploaded and adapts its entire extraction pipeline to match.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Generic Extraction
&lt;/h2&gt;

&lt;p&gt;Here's a sentence you might find in a clinical document:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Patient with hypertension started on lisinopril 10mg daily. The ACE inhibitor is contraindicated with potassium supplements. Side effects include dry cough and dizziness.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A generic extraction pipeline -- the kind most tools use -- will pull out a handful of entities and connect them with whatever relationship labels the LLM feels like inventing. You might get "Lisinopril" typed as an &lt;strong&gt;Item&lt;/strong&gt;, "Hypertension" as a &lt;strong&gt;Concept&lt;/strong&gt;, and "Dry Cough" as another &lt;strong&gt;Concept&lt;/strong&gt;. The relationships between them? Probably &lt;code&gt;related_to&lt;/code&gt; and &lt;code&gt;influences&lt;/code&gt;. Maybe &lt;code&gt;associated_with&lt;/code&gt; if you are lucky.&lt;/p&gt;

&lt;p&gt;This is the "garbage in, garbage out" of knowledge graphs. It's not that the AI failed to read the text. It read it fine. The problem is that nobody told it what to look for, what types are valid, or what the relationships between those types should mean.&lt;/p&gt;

&lt;p&gt;The graph you get is technically correct and practically useless. You cannot query "which drugs treat hypertension" because the system does not know what a Drug is. You cannot find contraindications because &lt;code&gt;related_to&lt;/code&gt; could mean anything. Every edge in the graph carries the same semantic weight as a shrug.&lt;/p&gt;

&lt;p&gt;Now run the same sentence through Chaos Cypher with the &lt;strong&gt;medical&lt;/strong&gt; domain active:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lisinopril&lt;/strong&gt; becomes a &lt;strong&gt;Drug&lt;/strong&gt; with dosage form and mechanism of action as properties&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hypertension&lt;/strong&gt; becomes a &lt;strong&gt;Condition&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dry Cough&lt;/strong&gt; and &lt;strong&gt;Dizziness&lt;/strong&gt; become &lt;strong&gt;Side Effects&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Potassium Supplements&lt;/strong&gt; gets recognized as a &lt;strong&gt;Drug&lt;/strong&gt; (because supplements have drug interactions too)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The relationships are just as precise: &lt;code&gt;treats&lt;/code&gt;, &lt;code&gt;contraindicated_with&lt;/code&gt;, &lt;code&gt;produces_side_effect&lt;/code&gt;. Each one is typed, directional, and constrained. A &lt;code&gt;treats&lt;/code&gt; edge must start from a therapeutic agent (Drug, Treatment, Procedure, Protocol, or Guideline) and land on a clinical target (Condition, Symptom, Side Effect, Pathogen, or Patient Population). A Side Effect can never treat a Drug. The LLM isn't guessing -- it's following a schema.&lt;/p&gt;

&lt;p&gt;That's what domain-aware extraction does. It turns a language model from a general-purpose pattern matcher into a domain specialist.&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%2Fax6my2fv0kg6bdttql56.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%2Fax6my2fv0kg6bdttql56.png" alt="Source detail showing entity and relationship distribution charts" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works: Upload to Knowledge Graph
&lt;/h2&gt;

&lt;p&gt;The workflow is straightforward. You upload a document. Chaos Cypher figures out what domain it belongs to, loads the right extraction rules, proposes the domain for your confirmation, then runs the pipeline. You don't need to configure anything upfront -- though you can override the detected domain if you want.&lt;/p&gt;

&lt;p&gt;:::info Update (June 2026)&lt;br&gt;
Since the domain-confirmation gate shipped, auto-detection proposes a domain and parks the source awaiting your one-click confirmation before the (potentially hour-long) extraction runs. You can accept, override the proposed domain, or pass &lt;code&gt;auto_confirm&lt;/code&gt; at upload to restore the fully hands-off behavior described below.&lt;br&gt;
:::&lt;/p&gt;

&lt;p&gt;Here's what happens behind the scenes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Detection&lt;/strong&gt; -- Chaos Cypher samples up to ~12,000 characters drawn from the beginning and the middle of your document and scores it against all registered domains simultaneously. Each domain has weighted keyword groups, regex patterns, and file type signals. The highest-scoring domain wins.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Guidance injection&lt;/strong&gt; -- The winning domain's extraction rules get injected into the LLM prompt. This includes entity type definitions, relationship constraints, exclusion rules (what &lt;em&gt;not&lt;/em&gt; to extract), and worked examples of correct extractions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strict type enforcement&lt;/strong&gt; -- The LLM is instructed to only use entity types from the domain's template list. After extraction, a code-level filter drops any entity whose type does not match a known template. No hallucinated types survive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Relationship validation&lt;/strong&gt; -- Each relationship is checked against source/target type constraints. A &lt;code&gt;treats&lt;/code&gt; relationship must flow from a therapeutic agent like a Drug, Treatment, or Procedure to a clinical target like a Condition or Symptom. Anything else gets rejected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality scoring&lt;/strong&gt; -- Extracted entities and relationships are scored by domain relevance. Domain-specific types like Drug and Condition score higher than generic fallbacks. This surfaces the most valuable parts of your graph.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Ff8zz473pp5qlkhncvd8m.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%2Ff8zz473pp5qlkhncvd8m.png" alt="Add Source dialog with URL input and file drag-and-drop" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chaos Cypher ships with &lt;strong&gt;19 built-in domains&lt;/strong&gt;, each tuned for a different category of document. A sample:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Typical Entity Types&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Medical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drug, Condition, Symptom, Procedure, Side Effect&lt;/td&gt;
&lt;td&gt;Clinical documents, pharmaceutical literature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Technical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Module, Class, Function, Endpoint, Design Pattern&lt;/td&gt;
&lt;td&gt;API docs, codebases, technical specifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Legal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Statute, Case, Party, Obligation, Legal Principle&lt;/td&gt;
&lt;td&gt;Contracts, court opinions, regulatory filings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Financial&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Company, Financial Instrument, Market Event, Regulation&lt;/td&gt;
&lt;td&gt;Earnings reports, market analysis, SEC filings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scientific&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hypothesis, Method, Finding, Dataset, Organism&lt;/td&gt;
&lt;td&gt;Research papers, experiments, academic publications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cybersecurity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Threat Actor, Vulnerability, Malware, Attack Technique&lt;/td&gt;
&lt;td&gt;Threat intel, incident reports, CVE research&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Person, Organization, Event, Concept, Location&lt;/td&gt;
&lt;td&gt;General-purpose fallback for any content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rest cover historical, literary, theological, investigative, political, design, intelligence, news, educational, philosophical, biographical, and reference material — the &lt;a href="https://chaoscypher.com/docs/user-guide/domains#built-in-domains" rel="noopener noreferrer"&gt;full list with every entity type is in the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every domain uses strict entity type enforcement by default. The medical domain defines 20 entity types. The technical domain has 18. These aren't suggestions -- they're the only types the LLM is allowed to produce. That constraint is what separates a clean, queryable graph from a noisy soup of ad-hoc labels.&lt;/p&gt;
&lt;h2&gt;
  
  
  Under the Hood: Domain Detection and Extraction Quality
&lt;/h2&gt;
&lt;h3&gt;
  
  
  How Detection Works
&lt;/h3&gt;

&lt;p&gt;Domain detection runs a scoring algorithm across all registered domains simultaneously. Each domain defines its detection rules in a JSON-LD config file with three signal types:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weighted keyword groups.&lt;/strong&gt; The medical domain has six keyword groups: &lt;code&gt;clinical_core&lt;/code&gt; (weight 1.2), &lt;code&gt;pharmaceutical&lt;/code&gt; (weight 1.0), &lt;code&gt;diagnostic&lt;/code&gt; (weight 0.9), &lt;code&gt;anatomy&lt;/code&gt; (weight 0.8), &lt;code&gt;procedures&lt;/code&gt; (weight 0.9), and &lt;code&gt;clinical_terms&lt;/code&gt; (weight 0.8). Each keyword match boosts the confidence score by &lt;code&gt;per_keyword_boost * weight&lt;/code&gt;. A document full of "diagnosis", "treatment", and "symptoms" racks up points fast in the clinical_core group, while scattered mentions of "cardiac" and "pulmonary" add smaller anatomy-weighted boosts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regex patterns.&lt;/strong&gt; Keywords catch common terms, but patterns catch domain-specific notation. The medical domain matches dosage expressions like &lt;code&gt;\d+\s*(mg|mcg|ml)&lt;/code&gt;, ICD codes like &lt;code&gt;ICD-10:J45&lt;/code&gt;, and prescription abbreviations like &lt;code&gt;b.i.d.&lt;/code&gt; and &lt;code&gt;p.r.n.&lt;/code&gt;. Each pattern match carries its own weight -- dosage notation at 1.4x, ICD codes at 1.5x. A single ICD code in a document is a strong medical signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File and document type signals.&lt;/strong&gt; File extensions (&lt;code&gt;.py&lt;/code&gt; for technical) and document type metadata (&lt;code&gt;medical_document&lt;/code&gt;, &lt;code&gt;openapi&lt;/code&gt;) provide additional boosts.&lt;/p&gt;

&lt;p&gt;The final confidence score is compared against a per-domain minimum threshold. Medical requires 0.4 minimum confidence. The generic domain has a threshold of 0.0 -- it always matches as a fallback, but with the lowest possible score (0.1), so any specialized domain that passes both its own minimum threshold and the registry's absolute confidence floor of 1.0 will win. The floor exists so weak, ambiguous matches fall back to the broad generic schema instead of locking in a wrong domain.&lt;/p&gt;
&lt;h3&gt;
  
  
  How Domains Shape Extraction Quality
&lt;/h3&gt;

&lt;p&gt;Detection picks the right domain. But the real value is in what happens next -- the selected domain controls the extraction pipeline at five points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entity guidance&lt;/strong&gt; tells the LLM what to extract &lt;em&gt;and what to skip&lt;/em&gt;. The medical domain says "include dosage information as properties on drug entities" and explicitly excludes standalone dosage numbers, study references ("Figure 1"), and administrative codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict type enforcement&lt;/strong&gt; gives the LLM a closed list of valid types -- the medical domain allows exactly 20. Anything outside that list gets dropped in post-processing. No more "Medical Concept" or "Health Thing" cluttering your graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationship constraints&lt;/strong&gt; validate source and target: &lt;code&gt;treats&lt;/code&gt; must flow from a therapeutic agent like a Drug, Treatment, or Procedure to a clinical target like a Condition or Symptom. A Symptom that "treats" a Drug fails validation. This catches the most common extraction error -- reversed or nonsensical edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility groups&lt;/strong&gt; handle deduplication: "Hypertension" extracted as a Condition in one chunk and a vaguer type in another can be merged because both belong to the &lt;code&gt;clinical&lt;/code&gt; group -- no duplicates, no lost type precision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property type mapping&lt;/strong&gt; rescues mistyped entities: "Severity" extracted as a standalone node gets absorbed into the right Condition as a &lt;code&gt;severity&lt;/code&gt; property instead of floating as an orphan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In plain English: the domain is a contract. The LLM does the reading, but the domain decides what counts as a valid entity, a valid relationship, and a valid graph -- and everything that breaks the contract is filtered out before it reaches you.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Every built-in domain is just a JSON-LD file. No Python, no compilation, no framework code. If you need a domain for your field that doesn't exist yet, you can create one in about 20 minutes: drop a file in &lt;code&gt;data/plugins/domains/&lt;/code&gt;, define your detection signals and your vocabulary, restart, done.&lt;/p&gt;

&lt;p&gt;Say you want a &lt;strong&gt;startup&lt;/strong&gt; domain for pitch decks and funding announcements. The skeleton is just three parts -- how to recognize the content, what entities exist, and how they're allowed to connect:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ExtractionDomain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"startup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strict_entity_types"&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;"detection"&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;"confidence"&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;"base_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"per_keyword_boost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.12&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;"keywords"&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;"funding"&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;"terms"&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;"series A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"series B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"seed round"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"venture capital"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="s2"&gt;"valuation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"term sheet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cap table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pre-money"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.3&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"patterns"&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;"regex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;$&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;d+[MBK]&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;s+(seed|series|round)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.5&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"templates"&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;"node_templates"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"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;"startup_company"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Company"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"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;"startup_round"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Funding Round"&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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"edge_templates"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"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;"startup_raised"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"raised"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"source_types"&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;"Company"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"target_types"&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;"Funding Round"&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="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="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;Detection has an absolute confidence floor of 1.0 -- a domain that scores below it loses to the generic fallback, so give a new domain enough keyword surface to clear it, or force the domain at upload while you tune detection. (The skeleton above clears it: eight keywords at a 0.12 boost and 1.3 weight put a keyword-rich article at roughly 1.45 before patterns even fire.)&lt;/p&gt;

&lt;p&gt;Upload a TechCrunch article and watch the detection engine propose it at the confirmation step: confirm, and Companies and Funding Rounds appear in your graph, connected only by the relationships you allowed.&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%2Fro66eab11skj43g66pg1.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%2Fro66eab11skj43g66pg1.png" alt="Source extraction view showing domain-specific entity types" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full walkthrough -- weighted keyword groups, named-referent flags, entity properties, inverse relationships, and a complete worked example -- lives in the docs: start with &lt;a href="https://chaoscypher.com/docs/user-guide/domains#custom-domains" rel="noopener noreferrer"&gt;Custom Domains&lt;/a&gt;, then &lt;a href="https://chaoscypher.com/docs/developer-guide/building-domains" rel="noopener noreferrer"&gt;Building Extraction Domains&lt;/a&gt; for the whole schema, including normalization keywords, compatibility groups, property absorption, and extraction-density tuning. The built-in medical domain is the most comprehensive reference -- study it when you want the full picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;We're planning more specialized domains -- supply chain, environmental science, and music theory are on the shortlist. But the real potential is in what users build. Every field has its own vocabulary, its own entity types, its own relationship patterns. A materials scientist cares about Crystal Structure, Synthesis Method, and Property. A genealogist needs Person, Family, Vital Record, and Census Entry. A cybersecurity analyst -- who already has a built-in domain -- might want to fork it and add types specific to their organization's threat model.&lt;/p&gt;

&lt;p&gt;If you build a domain for your field, share it. A JSON-LD file is small, portable, and easy to review. Drop it in &lt;code&gt;data/plugins/domains/&lt;/code&gt; and it works. No pull request required to use it, but we would love to include community domains in the built-in set for others to benefit from.&lt;/p&gt;

&lt;p&gt;Domains work identically whether you're running &lt;a href="https://chaoscypher.com/blog/local-ai-knowledge-graph" rel="noopener noreferrer"&gt;locally with Ollama&lt;/a&gt; or with a cloud provider. Start with the skeleton above, test it on your documents, and iterate from there.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>graphrag</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>Give Any AI Assistant Direct Access to Your Knowledge Graph with MCP</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:45:42 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/give-any-ai-assistant-direct-access-to-your-knowledge-graph-with-mcp-18b2</link>
      <guid>https://dev.to/chaoscypherinc/give-any-ai-assistant-direct-access-to-your-knowledge-graph-with-mcp-18b2</guid>
      <description>&lt;p&gt;Your knowledge graph is stuck in a browser tab. You built something valuable -- a map of entities, relationships, and source documents that represents real understanding of a domain. But the moment you switch to Claude to write a report, or open Cursor to write code, or ask ChatGPT to help with analysis, that knowledge graph might as well not exist. You're back to copying text, pasting context, and manually cross-referencing. Two tools that should be working together are stuck in separate worlds.&lt;/p&gt;

&lt;p&gt;Chaos Cypher now speaks MCP, which means any AI assistant that supports the protocol -- Claude Desktop, Claude Code, Cursor, Windsurf, and a growing list of others -- can directly query, search, traverse, and even write to your knowledge graph. No copy-paste. No context switching. Just ask.&lt;/p&gt;

&lt;p&gt;This post walks through what that actually looks like, what's under the hood, and how to set it up in about two minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is MCP, and Why Should You Care?
&lt;/h2&gt;

&lt;p&gt;MCP stands for Model Context Protocol. Anthropic released it as an open standard, and the simplest analogy is USB-C for AI tools. Before USB-C, every device had its own charger, its own cable, its own connector. MCP does the same thing for AI integrations: it defines one protocol that any AI host can use to talk to any tool server.&lt;/p&gt;

&lt;p&gt;Instead of building a custom plugin for Claude, another for ChatGPT, another for Cursor, and another for every new AI tool that launches next month, you build one MCP server. Every compatible AI tool can use it immediately.&lt;/p&gt;

&lt;p&gt;The adoption has been fast. Claude Desktop, Claude Code, Cursor, Windsurf, Cline, and Continue all support MCP today. The protocol handles tool discovery (the AI asks "what can you do?"), tool invocation (the AI calls a function with parameters), and result streaming. From the AI's perspective, your knowledge graph becomes just another set of capabilities it can use to answer questions.&lt;/p&gt;

&lt;p&gt;From your perspective, it means you stop being the middleman between your data and your AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;The best way to understand MCP is to see the before and after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before MCP:&lt;/strong&gt; You have a knowledge graph with 200 entities extracted from research papers on gene therapy. You're writing a literature review in Claude. To reference your graph, you open Chaos Cypher in another tab, run a search, copy the results, paste them into Claude, ask your question, realize you need more context, go back to the graph, find related entities, copy those too, paste again. Repeat until frustrated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After MCP:&lt;/strong&gt; You tell Claude: "Search my knowledge graph for all entities related to CRISPR and find the shortest path to gene therapy applications." Claude calls &lt;code&gt;graphrag_search&lt;/code&gt; to find relevant entities and document passages, then calls &lt;code&gt;find_shortest_path&lt;/code&gt; to trace the relationship chain. You get a grounded answer with specific entities and relationships from your own research, in one turn.&lt;/p&gt;

&lt;p&gt;Here are three scenarios that show the range of what's possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 1: Research -- Connecting the Dots
&lt;/h3&gt;

&lt;p&gt;You've been building a knowledge graph from papers on quantum computing and machine learning. You're deep in a writing session in Claude Desktop and want to understand where these two fields intersect in your collected research.&lt;/p&gt;

&lt;p&gt;You ask: &lt;em&gt;"What are the connections between quantum computing and machine learning in my research? Show me the key entities and how they're related."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Claude calls &lt;code&gt;search_nodes&lt;/code&gt; to find nodes matching both topics, then &lt;code&gt;get_node_context&lt;/code&gt; to pull the immediate neighborhood of the most central ones, including the edges that connect them and the source document chunks that support each relationship. You get back a structured map of how your research connects these fields -- not a generic internet answer, but one grounded in the specific papers you've indexed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Coding -- Your Project's Knowledge Base in Your Editor
&lt;/h3&gt;

&lt;p&gt;You're in Cursor, working on a codebase that has an associated knowledge graph mapping its architecture -- services, APIs, data flows, dependencies. You need to understand how the authentication service connects to the billing pipeline.&lt;/p&gt;

&lt;p&gt;You ask: &lt;em&gt;"Traverse from the Authentication Service node to anything related to billing. What's the path?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Cursor calls &lt;code&gt;resolve_node&lt;/code&gt; to find the canonical node for "Authentication Service" (even if you didn't remember the exact label), then &lt;code&gt;traverse_path&lt;/code&gt; to walk the graph two hops out, filtered to the relevant edge types. You see the chain: Authentication Service -&amp;gt; User Session -&amp;gt; Subscription Manager -&amp;gt; Billing Pipeline. Without leaving your editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: Writing -- Summarize With Citations
&lt;/h3&gt;

&lt;p&gt;You're drafting a report and need to summarize everything in your knowledge graph about a specific topic, with citations back to the original source documents.&lt;/p&gt;

&lt;p&gt;You ask: &lt;em&gt;"Summarize all my sources related to climate policy in the European Union. Include which documents each claim comes from."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Claude calls &lt;code&gt;get_summary_context&lt;/code&gt; to retrieve and cluster document chunks relevant to the query. Because this tool returns the raw chunks with their source metadata rather than making an LLM call, Claude itself does the summarization -- giving you a synthesis grounded in your documents, with each claim traced back to a specific source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood: 31 Tools, 7 Categories
&lt;/h2&gt;

&lt;p&gt;Chaos Cypher exposes 31 tools through MCP, organized into seven categories: &lt;strong&gt;GraphRAG search&lt;/strong&gt; (the flagship -- Personalized PageRank fused with hybrid vector/keyword retrieval), &lt;strong&gt;nodes&lt;/strong&gt;, &lt;strong&gt;edges&lt;/strong&gt;, &lt;strong&gt;templates&lt;/strong&gt;, &lt;strong&gt;analytics&lt;/strong&gt; (shortest paths, similarity, community detection, multi-hop traversal), &lt;strong&gt;documents&lt;/strong&gt; (upload, status, summarization context), and &lt;strong&gt;client-driven extraction&lt;/strong&gt; -- where the AI assistant reads chunks, extracts entities itself, and submits them back, no server LLM required. The &lt;a href="https://chaoscypher.com/docs/user-guide/mcp#available-tools" rel="noopener noreferrer"&gt;full tool reference is in the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The design principle: read operations are always safe and always available, write operations are opt-in. &lt;strong&gt;19 tools are read-only; 12 require write mode to be explicitly enabled&lt;/strong&gt; by a single setting. If you're not comfortable with an AI modifying your graph, leave it in read mode -- the AI can still search, traverse, and analyze everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two transport modes:&lt;/strong&gt; The MCP server runs in two ways depending on your setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stdio&lt;/strong&gt; -- For desktop AI tools like Claude Desktop and Cursor. The CLI starts a server that communicates over standard input/output. No network involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streamable HTTP&lt;/strong&gt; -- For the Docker stack. The Cortex API exposes MCP at &lt;code&gt;/api/v1/mcp&lt;/code&gt; using the Streamable HTTP transport, so MCP clients on your network can connect after authenticating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both transports expose the same 31 tools with the same behavior. The only difference is how they're connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you have the Chaos Cypher CLI installed, connecting Claude Code is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add chaoscypher &lt;span class="nt"&gt;--&lt;/span&gt; chaoscypher mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Claude Desktop, add the equivalent two-line entry to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"chaoscypher"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chaoscypher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"mcp"&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="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;That's genuinely it -- restart the client and Chaos Cypher's tools appear automatically. Cursor and other stdio clients use the same two-line config, and if you run the Docker stack, the MCP endpoint is &lt;em&gt;already live&lt;/em&gt; at &lt;code&gt;http://localhost/api/v1/mcp&lt;/code&gt; for any Streamable-HTTP client that signs in with your Chaos Cypher credentials -- it sits behind the same edge authentication as the rest of the API (or &lt;code&gt;http://localhost:8080/api/v1/mcp&lt;/code&gt; if you run the multi-container dev stack, which publishes Cortex's port directly). Per-client walkthroughs are in the &lt;a href="https://chaoscypher.com/docs/user-guide/mcp#setup" rel="noopener noreferrer"&gt;MCP setup docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By default the server runs in read-only mode; flip &lt;code&gt;mcp.mode: write&lt;/code&gt; in &lt;code&gt;settings.yaml&lt;/code&gt; to enable the 12 write tools, and use &lt;code&gt;chaoscypher mcp --database my-research&lt;/code&gt; to point at a specific database. The &lt;a href="https://chaoscypher.com/docs/user-guide/mcp#configuration" rel="noopener noreferrer"&gt;configuration reference&lt;/a&gt; covers the rest, including &lt;code&gt;auto_extract&lt;/code&gt; for documents uploaded via MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Data Stays Local
&lt;/h2&gt;

&lt;p&gt;This is worth stating explicitly: MCP doesn't send your knowledge graph data to any external service. The protocol is a local communication channel between the AI tool running on your machine and the Chaos Cypher server running on your machine (or your network, if you use Docker). When Claude calls &lt;code&gt;graphrag_search&lt;/code&gt;, the query goes from Claude to your local MCP server, your server searches your local database, and the results go back to Claude. Your documents, entities, and relationships never leave your infrastructure.&lt;/p&gt;

&lt;p&gt;The AI model itself runs wherever it runs -- that's between you and your provider. But the knowledge graph data stays entirely under your control. If you pair Chaos Cypher with a local model via Ollama, the entire pipeline is air-gapped. See our &lt;a href="https://chaoscypher.com/blog/local-ai-knowledge-graph" rel="noopener noreferrer"&gt;local AI setup guide&lt;/a&gt; for the full walkthrough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;MCP support is the foundation for a broader vision: your knowledge graph as a persistent layer that any tool in your workflow can tap into. Here's what's on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt templates&lt;/strong&gt; -- Pre-built MCP prompts for common patterns like "summarize this topic with citations" or "find contradictions in my sources," so you don't have to craft the right question every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource exposure&lt;/strong&gt; -- Making graph nodes and documents available as MCP resources, so AI tools can browse your knowledge graph like a file system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-database switching&lt;/strong&gt; -- Seamlessly switch between knowledge graphs within a single MCP session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flagship &lt;code&gt;graphrag_search&lt;/code&gt; tool deserves its own explanation -- it's doing a lot more than keyword lookup. Read &lt;a href="https://chaoscypher.com/blog/graphrag-enhanced-search" rel="noopener noreferrer"&gt;how GraphRAG works&lt;/a&gt; for the full deep-dive on the retrieval pipeline.&lt;/p&gt;

&lt;p&gt;The MCP server ships with Chaos Cypher today. If you're already running it, you have it -- just configure your AI tool and go.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; Full MCP setup guide and tool reference in the &lt;a href="https://chaoscypher.com/docs/user-guide/mcp" rel="noopener noreferrer"&gt;docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; The MCP implementation lives in the &lt;code&gt;chaoscypher_core.mcp&lt;/code&gt; package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issues:&lt;/strong&gt; Found a bug or have a feature request? &lt;a href="https://github.com/chaoscypherinc/chaoscypher/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; or &lt;a href="https://github.com/chaoscypherinc/chaoscypher/discussions" rel="noopener noreferrer"&gt;start a discussion&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between "having a knowledge graph" and "using a knowledge graph" has always been the friction of switching contexts. MCP closes that gap. Your knowledge graph is no longer a destination you visit -- it's a capability that follows you into whatever tool you're already working in.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>graphrag</category>
      <category>ai</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Why Your RAG Chat is Missing Half the Answers (And How GraphRAG Fixes It)</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:45:41 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/why-your-rag-chat-is-missing-half-the-answers-and-how-graphrag-fixes-it-1k89</link>
      <guid>https://dev.to/chaoscypherinc/why-your-rag-chat-is-missing-half-the-answers-and-how-graphrag-fixes-it-1k89</guid>
      <description>&lt;p&gt;You upload four research papers to your RAG chatbot. You ask: "How does Dr. Chen's CRISPR research connect to the gene therapy trials at Stanford?" The chatbot thinks for a moment and gives you... a paragraph about CRISPR. Generic, shallow, pulled from whichever single chunk happened to mention the word. The actual answer -- that Chen published a paper on CRISPR delivery mechanisms, which was cited by a Stanford clinical trial for retinal gene therapy, which built on a funding collaboration between both institutions -- exists across three different documents. Your chatbot never even tried to find it.&lt;/p&gt;

&lt;p&gt;This is the multi-hop problem, and it's the silent failure mode of every vector-only RAG system. Vector search embeds your question, compares it against document chunks, and returns the closest matches by cosine similarity. It works for single-hop questions: "What is CRISPR?" or "When did the Stanford trial begin?" But the moment an answer requires connecting information across documents -- following a citation chain, tracing a person through multiple sources, linking a cause in one report to an effect in another -- vector search falls apart. It can't follow relationships. It doesn't know that entities in different documents refer to the same thing. It just sees text. In an independent benchmark, GraphRAG-style retrieval scored 53% vs 43% for reranked vector RAG on complex multi-hop reasoning (&lt;a href="https://arxiv.org/abs/2506.05690" rel="noopener noreferrer"&gt;GraphRAG-Bench, ICLR 2026&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The worst part: it fails silently. No error message, no "I couldn't find a complete answer." You get a confident-sounding response that happens to be shallow or wrong.&lt;/p&gt;

&lt;p&gt;Chaos Cypher's GraphRAG search fixes this by fusing knowledge graph traversal with vector search. When you ask a multi-hop question, it walks the graph of entities and relationships extracted from your documents, finds structurally connected information you didn't ask about, retrieves the source passages that prove those connections, and merges everything into a single ranked result set. The answer you get isn't just semantically similar text. It's the actual chain of evidence.&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%2Fyeha9i1bezyqe2x3nq8e.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%2Fyeha9i1bezyqe2x3nq8e.png" alt="Search results showing entities with relevance scores and type badges" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When You Ask a Multi-Hop Question
&lt;/h2&gt;

&lt;p&gt;Let's walk through a real scenario. You have uploaded three documents into Chaos Cypher: a research paper by Dr. Sarah Chen on CRISPR delivery vectors, a Stanford clinical trial report on retinal gene therapy, and a grant proposal connecting both institutions. You type into the chat: "How does Chen's CRISPR work relate to the Stanford gene therapy trial?"&lt;/p&gt;

&lt;p&gt;Here's what happens behind the scenes, in seven steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Embed the query.&lt;/strong&gt; Your question gets converted into a vector embedding -- the same starting point as any RAG system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Match seed entities.&lt;/strong&gt; Instead of immediately searching document chunks, GraphRAG first searches the knowledge graph. It finds entities whose embeddings are closest to your query vector. In this case, it matches "Dr. Sarah Chen" (a Person node) and "CRISPR delivery vectors" (a Concept node) as high-confidence seeds -- the anchor points for graph exploration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Personalized PageRank.&lt;/strong&gt; This is where it gets interesting. Standard PageRank finds globally important nodes. Personalized PageRank is different: it starts from your seed entities and performs a biased random walk through the graph. At each step, there is an 85% chance of following a relationship to a neighbor, and a 15% chance of teleporting back to a seed. Entities structurally close to your seeds get high scores, even if they were never mentioned in your query.&lt;/p&gt;

&lt;p&gt;In our example, the algorithm discovers that "Dr. Sarah Chen" has a "published" relationship to "Lipid Nanoparticle Delivery Study," which has a "cited_by" edge pointing to "Stanford Retinal Gene Therapy Trial Phase II," which in turn has a "funded_by" connection to "NIH CRISPR Therapeutics Grant" -- a grant that also lists Chen as a co-investigator. None of these intermediate entities matched your query by text similarity. The graph surfaced them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Assemble graph context.&lt;/strong&gt; The top-scoring entities from PageRank are collected along with their relationships. This produces a structured context: seed entities you asked about, related entities the graph discovered, and the relationship triples connecting them. This context gets passed to the language model alongside the document chunks, giving it the structural "map" it needs to reason about connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Retrieve provenance chunks.&lt;/strong&gt; The first of two independent retrieval paths. For each entity the graph surfaced, GraphRAG looks up which document chunks those entities were originally extracted from. Chen was extracted from page 3 of the research paper. The Stanford trial came from the clinical report abstract. The funding connection came from page 12 of the grant proposal. These "provenance chunks" contain the actual evidence for the graph relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Retrieve vector chunks.&lt;/strong&gt; The second path runs next -- standard hybrid search (semantic + keyword) against all document chunks. It catches relevant passages that might not have generated graph entities but still contain useful context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Merge and rank.&lt;/strong&gt; The two paths produce two independently ranked lists. GraphRAG merges them using Reciprocal Rank Fusion, which combines rankings without normalizing scores across systems. Chunks appearing in both lists get a combined boost. The result is a single, deduplicated, ranked list of the most relevant passages across all your documents.&lt;/p&gt;

&lt;p&gt;Instead of a shallow answer about CRISPR, you get the full chain: Chen's delivery mechanism research led to a cited clinical application at Stanford, connected through shared funding. The chat response includes both the graph context (discovered entities and relationships) and the document passages that prove those connections.&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%2Ftcw8xmvu3e9i7ja3qzf4.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%2Ftcw8xmvu3e9i7ja3qzf4.png" alt="Knowledge graph with search highlighting entity paths" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood (Technical Deep-Dive)
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This section is for developers who want to understand the algorithms. Skip ahead to "Try It Yourself" if you just want to use it.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Personalized PageRank
&lt;/h3&gt;

&lt;p&gt;Standard PageRank models a "random surfer" following links uniformly across a network. Personalized PageRank changes one thing: instead of teleporting to a random node, the surfer teleports back to seed nodes. This transforms a global importance metric into a query-specific relevance metric.&lt;/p&gt;

&lt;p&gt;Chaos Cypher's implementation uses rustworkx's compiled power iteration with a 0.85 damping factor and runs entirely in-process -- no external graph database or service required. The seed weights come from the vector similarity scores in Step 2, so the random walk isn't just seeded on the right entities -- it's biased toward the ones most relevant to your specific question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reciprocal Rank Fusion
&lt;/h3&gt;

&lt;p&gt;Provenance chunks have graph-connectivity scores. Vector chunks have cosine similarity scores. These aren't on the same scale, so you can't just sort by score.&lt;/p&gt;

&lt;p&gt;RRF (Cormack, Clarke &amp;amp; Butt, 2009) sidesteps this by ignoring scores entirely and using only rank positions. Each chunk's RRF score is the sum of &lt;code&gt;1 / (k + rank)&lt;/code&gt; across all lists where it appears. The smoothing constant &lt;code&gt;k&lt;/code&gt; (60, matching the original paper) dampens the advantage of being ranked first versus second.&lt;/p&gt;

&lt;p&gt;The key property: chunks appearing in both lists get contributions from both, naturally boosting results validated by two independent signals. A chunk ranked 5th in provenance and 8th in vector search will often outrank one that is 1st in vector but absent from provenance. Evidence confirmed by graph structure is worth more than text similarity alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graceful Degradation
&lt;/h3&gt;

&lt;p&gt;Not every database has a knowledge graph. Not every query matches graph entities. GraphRAG picks its operating mode automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;full_graphrag&lt;/code&gt;&lt;/strong&gt; -- Seeds found, PPR succeeded. Graph context + provenance chunks + vector chunks + RRF fusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vector_only&lt;/code&gt;&lt;/strong&gt; -- Embeddings work but no graph seeds found. Standard hybrid search, no graph context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;keyword_only&lt;/code&gt;&lt;/strong&gt; -- Embeddings unavailable. Pure SQLite FTS keyword search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system never fails -- it always returns the best results it can. The retrieval stats in each response tell you exactly what happened: mode used, seeds found, entities explored, provenance versus vector chunk counts.&lt;/p&gt;

&lt;p&gt;In plain English: the graph finds the &lt;em&gt;path&lt;/em&gt; between things you asked about, the vector index finds &lt;em&gt;text&lt;/em&gt; that sounds like your question, and the fusion step trusts evidence confirmed by both more than either alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Here's the good news: you don't need to configure anything. GraphRAG is the default search mode behind every chat conversation in Chaos Cypher. When you type a question, the chat system prioritizes &lt;code&gt;graphrag_search&lt;/code&gt; as its first tool for most questions. If your database has extracted entities and embeddings, you get the full pipeline. If not, it degrades gracefully to vector or keyword search.&lt;/p&gt;

&lt;p&gt;The simplest way to see it in action:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Upload 3-4 related documents.&lt;/strong&gt; Pick sources that share entities -- research papers from the same field, chapters from the same book, reports about the same project. The key is overlap: the documents should reference some of the same people, organizations, concepts, or events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wait for extraction to complete.&lt;/strong&gt; Chaos Cypher will chunk the documents, generate embeddings (automatic), and then you can optionally run entity extraction to build the knowledge graph. (Update, May 2026: extraction now auto-detects your document's domain first and asks you to confirm it before the run starts -- you can also pre-confirm in the upload dialog.) The extraction step is what creates the graph nodes and edges that GraphRAG traverses. Without it, you still get vector-only search, which is fine -- but you miss the multi-hop connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ask a question that spans documents.&lt;/strong&gt; Don't ask something that a single document can answer. Ask about connections: "How does X relate to Y?" or "What is the link between the findings in paper A and the methodology in paper B?" This is where GraphRAG earns its keep.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check the retrieval stats.&lt;/strong&gt; In the chat response metadata, you'll see the retrieval mode (&lt;code&gt;full_graphrag&lt;/code&gt;, &lt;code&gt;vector_only&lt;/code&gt;, or &lt;code&gt;keyword_only&lt;/code&gt;), the number of seed entities found, how many entities PageRank explored, and the breakdown of provenance versus vector chunks. This tells you exactly what the pipeline did for your query.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;GraphRAG is also available as an MCP tool called &lt;code&gt;graphrag_search&lt;/code&gt;, meaning any AI assistant that supports MCP can use it directly against your Chaos Cypher instance. See our &lt;a href="https://chaoscypher.com/blog/mcp-server-launch" rel="noopener noreferrer"&gt;MCP launch post&lt;/a&gt; for setup instructions with Claude Desktop, Cursor, and others.&lt;/p&gt;

&lt;p&gt;The pipeline is tunable -- seed similarity threshold, PageRank top-K and damping, triple limits -- via a &lt;code&gt;graphrag&lt;/code&gt; section in &lt;code&gt;settings.yaml&lt;/code&gt;, though the defaults were chosen from the GraphRAG literature and testing across database sizes, and most users will never need to touch them. The full parameter reference is in the &lt;a href="https://chaoscypher.com/docs/user-guide/search#graphrag-tuning" rel="noopener noreferrer"&gt;search documentation&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fax02jopz0ihmszz4b587.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%2Fax02jopz0ihmszz4b587.png" alt="Chat conversation with AI response and source citations" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;GraphRAG in Chaos Cypher today handles local queries well -- questions where you have a specific starting point and want to follow connections outward. But there's a class of questions it doesn't yet handle optimally: corpus-wide questions like "What are the main themes across all my documents?" or "Summarize everything related to sustainability."&lt;/p&gt;

&lt;p&gt;These require what the research literature calls community summaries -- pre-computed summaries of entity clusters in the graph that can answer high-level questions without traversing the entire structure at query time. That's on the roadmap.&lt;/p&gt;

&lt;p&gt;If you're working with a use case where multi-hop retrieval matters -- legal discovery, academic research, intelligence analysis, medical literature review -- we'd love to hear about your experience. What kinds of multi-hop questions does your work require? Where does the current pipeline fall short? The best way to reach us is through the project's GitHub discussions.&lt;/p&gt;

&lt;p&gt;For a deeper look at the architecture, see the &lt;a href="https://chaoscypher.com/docs/user-guide/search" rel="noopener noreferrer"&gt;Search documentation&lt;/a&gt; and the &lt;a href="https://chaoscypher.com/docs/getting-started/overview" rel="noopener noreferrer"&gt;Architecture overview&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>graphrag</category>
      <category>rag</category>
      <category>ai</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>ChaosCypher v0.4.2: The Streaming Bug That Only Showed Up When the Chat Was Working</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Wed, 09 Sep 2026 00:26:29 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/chaoscypher-v042-the-streaming-bug-that-only-showed-up-when-the-chat-was-working-ejg</link>
      <guid>https://dev.to/chaoscypherinc/chaoscypher-v042-the-streaming-bug-that-only-showed-up-when-the-chat-was-working-ejg</guid>
      <description>&lt;p&gt;v0.4.2 adds nothing. No new features, no breaking API changes, no schema migrations — 61 commits of fixes on top of v0.4.1. If you self-host ChaosCypher, three of them matter enough to upgrade for: live chat streaming works again, a queued task could create database files wherever it liked, and the API stopped shipping whole payloads on every poll.&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%2F02uolct3m935zqakcdhn.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%2F02uolct3m935zqakcdhn.png" alt="Knowledge graph visualization showing extracted entities and relationships" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug that hid behind a working feature
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Live chat streaming was dead, and the product looked fine anyway.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The middleware that disconnects request-scoped storage adapters ran its teardown as soon as the response &lt;em&gt;object&lt;/em&gt; existed — not when the response had finished sending. On &lt;code&gt;GET /chats/{id}/events&lt;/code&gt; the generator's first suspension is the pub/sub subscribe, and the reconcile right after it hit an adapter that had just been disconnected. So every live stream died with &lt;code&gt;STREAM_INTERNAL_ERROR&lt;/code&gt; before relaying a single token, whenever the chat was actually processing.&lt;/p&gt;

&lt;p&gt;What made it survive review is what happened next: the answer still appeared when you reloaded. The worker had run, the turn was persisted, the data was correct. Only the live token-by-token experience was gone — which reads as "streaming feels broken today" rather than "a middleware is tearing down storage under an open response".&lt;/p&gt;

&lt;p&gt;The middleware is now pure ASGI and tears down after the whole response is sent. That fixes the class, not just the symptom: any streaming endpoint or background task that touches an adapter after the response object is produced was exposed to the same teardown. It previously had no tests at all; it now has seven, two of which fail against the old implementation.&lt;/p&gt;

&lt;p&gt;In plain English: chat answers stream live again, and the piece of plumbing that broke them is now covered by tests.&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%2Fax02jopz0ihmszz4b587.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%2Fax02jopz0ihmszz4b587.png" alt="Chat conversation with AI response and source citations" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Three security fixes worth reading if you run this on a LAN
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A queued task could create SQLite files at a path of its choosing.&lt;/strong&gt; &lt;code&gt;metadata.database_name&lt;/code&gt; on &lt;code&gt;POST /api/v1/queue/tasks&lt;/code&gt; reached a bare &lt;code&gt;Path&lt;/code&gt; join in &lt;code&gt;get_db_path&lt;/code&gt;, which then &lt;code&gt;mkdir&lt;/code&gt;'d and created a database file wherever the value pointed. The sink now enforces the same &lt;code&gt;[A-Za-z0-9_-]+&lt;/code&gt; fullmatch that &lt;code&gt;BackupService&lt;/code&gt; and the queue handlers already applied — placed at the sink, so it covers every reader of that metadata field at once rather than one caller at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The health endpoint was an unthrottled bcrypt oracle.&lt;/strong&gt; &lt;code&gt;location = /api/v1/health&lt;/code&gt; was the one &lt;code&gt;auth_request&lt;/code&gt; location in the nginx templates without a &lt;code&gt;limit_req&lt;/code&gt;, so an unauthenticated host on your LAN could drive one bcrypt hash (cost 12) per stored API key, per request. It now carries the same rate limit as its siblings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-IP auth rate limiting now enforces the policy it declares.&lt;/strong&gt; This one was two broken halves of a single control. &lt;code&gt;proxy-public.conf&lt;/code&gt; blanked &lt;code&gt;X-Auth-Edge-Token&lt;/code&gt; on exactly the public auth routes, which collapsed the app layer's per-IP login and setup buckets into one global bucket keyed to the nginx loopback peer — so any host on your network could starve your own login. Separately, the nginx &lt;code&gt;auth&lt;/code&gt; zone rendered &lt;code&gt;login_max_requests&lt;/code&gt; as requests &lt;em&gt;per second&lt;/em&gt;, 60× the configured per-window policy, while every &lt;code&gt;*_window_seconds&lt;/code&gt; setting was silently ignored and &lt;code&gt;setup_max_requests&lt;/code&gt; never rendered at all, because both locations shared one zone. Zones now render as floored requests-per-minute, &lt;code&gt;/setup&lt;/code&gt; gets its own zone, and burst scales with the per-window count.&lt;/p&gt;

&lt;p&gt;Worth stating plainly, because the changelog does: the rate-limiting defect was an availability problem, not an auth bypass — &lt;code&gt;X-Auth-User&lt;/code&gt; was never trusted from outside the edge.&lt;/p&gt;

&lt;p&gt;Riding along: MCP read mode no longer leaks five write tools that were missing from &lt;code&gt;TOOL_DEFINITIONS&lt;/code&gt; and fell through to &lt;code&gt;bridge.execute&lt;/code&gt;; &lt;code&gt;extract_archive()&lt;/code&gt; enforces the member-count, declared-total, and streamed-byte caps at the function boundary where untrusted archives actually land; credentials-file updates take a cross-process lock, so with &lt;code&gt;uvicorn_workers &amp;gt; 1&lt;/code&gt; a logout can no longer be clobbered back into validity; and a settings PATCH of &lt;code&gt;allowed_origins: ["*"]&lt;/code&gt; with &lt;code&gt;allow_credentials: true&lt;/code&gt; is rejected with a 422 instead of persisting and then making every subsequent boot &lt;code&gt;SystemExit&lt;/code&gt; with no API path back in.&lt;/p&gt;

&lt;p&gt;In plain English: a few things that were reachable from your local network, and one setting that could brick a restart, are closed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Work that reported success while disappearing
&lt;/h3&gt;

&lt;p&gt;The data-correctness fixes in this release share a property: each one logged or displayed success while losing, hiding, or overwriting something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A task failing before handler dispatch was silently lost.&lt;/strong&gt; The queue worker's outer &lt;code&gt;try&lt;/code&gt; had no &lt;code&gt;except&lt;/code&gt;, the done-callback never retrieved the task exception, and the &lt;code&gt;finally&lt;/code&gt; removed the task from the &lt;code&gt;running&lt;/code&gt; set while its hash still read &lt;code&gt;queued&lt;/code&gt;. The task then existed in neither pending nor running — invisible to the reconciler and to rehydration. It never ran and never reported. Such a task is now marked failed-terminal (visible, dead-lettered) and the poller logs the exception through the canonical path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A search-index sweep could clobber a permanently failed source back to &lt;code&gt;indexed&lt;/code&gt;.&lt;/strong&gt; The exhaustion branch deletes its queue row before marking the source failed, so a sibling draining later in the same batch saw zero survivors and flipped the source to &lt;code&gt;indexed&lt;/code&gt; — hiding a source that would never be searchable. Both indexed-flip sites now refuse to overwrite a terminal &lt;code&gt;failed&lt;/code&gt; status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirming a source with a forced domain could extract under the wrong one.&lt;/strong&gt; &lt;code&gt;forced_domain&lt;/code&gt; and other non-&lt;code&gt;None&lt;/code&gt; overrides were written in a second transaction after the atomic claim, while &lt;code&gt;gate_decision&lt;/code&gt; short-circuits on the claim timestamp alone. An import analysis snapshotting inside that window saw a confirmed-but-domainless source and extracted with the auto-detected domain instead of the one you chose. Everything now rides the single write-once claim.&lt;/p&gt;

&lt;p&gt;Alongside those: plain chat send was the only turn-enqueue path still missing the double-enqueue guard; the stuck-chat sweeper could stamp an error over an answer that had just completed; a pause landing mid-health-tick was re-attributed to &lt;code&gt;health_monitor&lt;/code&gt; and later auto-lifted; vision page retry counters could drift permanently into &lt;code&gt;completed + failed &amp;lt; total_pages&lt;/code&gt;; &lt;code&gt;reset_all()&lt;/code&gt; could leave the app pointed at a schema that no longer existed; resetting the knowledge base could leave you with zero templates; and the migration lock file is no longer unlinked while a waiter holds it, which had let a third process lock a fresh inode and run backup-and-upgrade concurrently with an in-flight upgrade.&lt;/p&gt;

&lt;p&gt;In plain English: several paths used to say "done" while quietly dropping or overwriting your work. They don't any more.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API got a lot lighter per poll
&lt;/h3&gt;

&lt;p&gt;None of this is a benchmark — it is a description of what the code was doing, from the changelog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every search ran a full 57-column source listing at page size 100,000.&lt;/strong&gt; &lt;code&gt;_get_enabled_source_ids()&lt;/code&gt; now uses a single-column projection through a new storage-protocol accessor, which also removes the &amp;gt;999-source SQLite parameter-limit hazard the old path could hit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GET /queue/tasks&lt;/code&gt; shipped whole task payloads — including LLM &lt;code&gt;messages&lt;/code&gt; arrays — on two five-second polls.&lt;/strong&gt; The list endpoint now returns a whitelisted subset (&lt;code&gt;inputs.filename&lt;/code&gt;, &lt;code&gt;inputs.analysis_depth&lt;/code&gt;, &lt;code&gt;operations_count&lt;/code&gt;). &lt;strong&gt;If you consume the list endpoint's &lt;code&gt;data&lt;/code&gt; blob from your own tooling, this is the one change in this release you may need to adjust for.&lt;/strong&gt; The detail endpoint is unchanged and still returns the full payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;get_source&lt;/code&gt; read and twice-copied the entire raw upload on a three-second poll.&lt;/strong&gt; &lt;code&gt;full_text&lt;/code&gt; now joins the heavy-column set the response model was discarding anyway, with a narrow accessor kept for the CCX export path that genuinely needs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk hydration batches instead of looping.&lt;/strong&gt; Four chat and workflow tool handlers replaced per-hit &lt;code&gt;get_chunk_by_id&lt;/code&gt; loops of up to ~100 queries per turn with a single batch fetch each, and the batch projection excludes the ~5 KB-per-hit embedding and the raw content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search hydration stops fetching embeddings it never reads&lt;/strong&gt; — and a follow-up applied the same projection to twelve more batch-fetch paths, none of which read the 1024-float embedding they were hydrating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In plain English: the screens that poll — queue, source detail, search — now ask the database for the columns they actually display.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you run the multi-container stack
&lt;/h3&gt;

&lt;p&gt;The multi-container login screen and setup wizard were broken: &lt;code&gt;multi-interface-nginx.conf&lt;/code&gt; had drifted from its template and was missing the auth-exempt &lt;code&gt;settings/public&lt;/code&gt; and &lt;code&gt;settings/host&lt;/code&gt; locations, so the SPA got 401s before login. The static config is re-synced.&lt;/p&gt;

&lt;p&gt;Also in that stack: the Valkey wipe sentinel is now written where the worker actually reads it (&lt;code&gt;/data/.valkey_was_wiped&lt;/code&gt;, not &lt;code&gt;/run/chaoscypher/...&lt;/code&gt;), so forced queue rehydration can fire after an AOF wipe; the boot splash ships its eight security headers, which nginx had been dropping through inherited &lt;code&gt;add_header&lt;/code&gt;; and the production compose file no longer points &lt;code&gt;LEXICON_URL&lt;/code&gt; at a dev-only host.&lt;/p&gt;

&lt;p&gt;In plain English: if you run the multi-container stack rather than the all-in-one image, this release repairs login, queue recovery after a Valkey wipe, and a few config defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading
&lt;/h3&gt;

&lt;p&gt;No migrations apply — this release has no schema changes at all.&lt;/p&gt;

&lt;p&gt;Drain the queue before swapping the image: stop new submissions and wait for &lt;code&gt;/api/v1/queue/stats&lt;/code&gt; to report 0 pending on all queues. Payload-version negotiation still isn't implemented, so don't run mixed versions against the same queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/chaoscypherinc/chaoscypher:0.4.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you run the Python packages directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; chaoscypher-core chaoscypher-cortex chaoscypher-neuron chaoscypher-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting fresh:&lt;br&gt;
&lt;/p&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;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; chaoscypher &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 443:443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; chaoscypher-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host.docker.internal:host-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/chaoscypherinc/chaoscypher:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Next steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Upgrade, then reopen a chat and watch an answer stream in — that is the fix you will notice first.&lt;/li&gt;
&lt;li&gt;If you read &lt;code&gt;GET /api/v1/queue/tasks&lt;/code&gt; from your own tooling, check whether you depended on fields outside &lt;code&gt;inputs.filename&lt;/code&gt;, &lt;code&gt;inputs.analysis_depth&lt;/code&gt;, and &lt;code&gt;operations_count&lt;/code&gt;; the detail endpoint still carries the full payload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full details in the &lt;a href="https://chaoscypher.com/docs/about/changelog" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;. ChaosCypher is AGPL-3.0 and local-first — the graph, the chat, the import and export paths all run on your own machine. Repo: &lt;a href="https://github.com/chaoscypherinc/chaoscypher" rel="noopener noreferrer"&gt;https://github.com/chaoscypherinc/chaoscypher&lt;/a&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>security</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Build a Private AI Knowledge Graph That Never Leaves Your Machine</title>
      <dc:creator>Denis MacPherson</dc:creator>
      <pubDate>Fri, 04 Sep 2026 14:53:23 +0000</pubDate>
      <link>https://dev.to/chaoscypherinc/build-a-private-ai-knowledge-graph-that-never-leaves-your-machine-47pe</link>
      <guid>https://dev.to/chaoscypherinc/build-a-private-ai-knowledge-graph-that-never-leaves-your-machine-47pe</guid>
      <description>&lt;p&gt;Every week, another AI tool asks you to upload your most sensitive documents to someone else's servers. Your contracts, medical records, internal research, personal journals -- all piped through APIs you don't control, stored in logs you can't audit, governed by terms of service that change without notice.&lt;/p&gt;

&lt;p&gt;For a lot of use cases, that's fine. But there's a whole class of knowledge that simply cannot leave your network. Healthcare organizations bound by HIPAA. Law firms handling privileged communications. Financial institutions with regulatory obligations around client data. Companies whose competitive advantage lives in proprietary research. Or maybe you just have a journal and you'd rather not feed your inner monologue to a data center in Virginia.&lt;/p&gt;

&lt;p&gt;The usual answer is "just don't use AI tools." That's not really an answer anymore. Chaos Cypher paired with Ollama runs a complete AI knowledge graph pipeline -- document ingestion, entity extraction, relationship mapping, semantic search, and conversational chat -- entirely on your local machine. No API keys. No usage limits. No data leaving your network. And it isn't a compromise or a toy demo: it's the same extraction pipeline, the same graph visualization, the same chat interface that works with cloud providers. You're just swapping the LLM backend from a remote API to a local one.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Zero to Local Knowledge Graph
&lt;/h2&gt;

&lt;p&gt;Here's the full workflow, start to finish. Fifteen minutes if you're following along, five if you've done this before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Install Ollama and pull a model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Head to &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;ollama.com&lt;/a&gt; and install it for your platform. Then pull a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull qwen3:30b-instruct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That downloads the model weights once. After that, Ollama runs as a local API server -- same REST interface as OpenAI, but pointing at &lt;code&gt;localhost:11434&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Start the Chaos Cypher stack.&lt;/strong&gt;&lt;br&gt;
&lt;/p&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;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; chaoscypher &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 443:443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; chaoscypher-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host.docker.internal:host-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/chaoscypherinc/chaoscypher:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pulls the published all-in-one image and starts it: the Cortex API server, a Neuron background worker, the web Interface, and Valkey for job queuing, all in one container. The image points the container at Ollama on your host machine through Docker's &lt;code&gt;host.docker.internal&lt;/code&gt; bridge; the &lt;code&gt;--add-host&lt;/code&gt; flag makes that name resolve on Linux Docker Engine (Docker Desktop resolves it on its own). No external network calls during operation -- the only downloads are the one-time Ollama model pull and a one-time fetch of the embedding model from HuggingFace at first indexing (cached afterwards; air-gapped installs can pre-seed the cache).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Upload a document.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;http://localhost&lt;/code&gt;, create a database (or use the default), and drag a PDF, DOCX, or text file into the Sources page. Chaos Cypher immediately begins indexing -- chunking the document, generating embeddings, and building a search index. This takes about 30 seconds for a 100-page PDF and requires no GPU at all (more on that below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Extract entities and relationships.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Indexing automatically queues entity extraction. Before the run starts, Chaos Cypher proposes the detected document domain and waits for one click of confirmation (the Review dialog, or pre-confirm in the upload wizard -- see the &lt;a href="https://chaoscypher.com/docs/getting-started/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;) -- confirm and the LLM gets to work: reading through each chunk, identifying entities (people, organizations, concepts, events), discovering relationships between them, and building a structured knowledge graph. The confirmed domain applies &lt;a href="https://chaoscypher.com/blog/domain-extraction-guide" rel="noopener noreferrer"&gt;domain-specific extraction rules&lt;/a&gt; for higher quality results. For a 100-page document with a 30B model, expect roughly 5-10 minutes.&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%2Fk8ut9syowczg1jpc3b80.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%2Fk8ut9syowczg1jpc3b80.png" alt="Sources list showing document processing status" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Chat with your knowledge graph.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once extraction finishes and the results are committed to your graph, open the Chat page and start asking questions. The chat system uses RAG (retrieval-augmented generation) to search your indexed documents and graph, then feeds the relevant context to your local LLM for a grounded answer. Everything stays on your machine -- the search, the retrieval, the generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick Your Preset
&lt;/h3&gt;

&lt;p&gt;Not everyone has the same GPU. Chaos Cypher ships with VRAM presets that auto-configure the right model, context window, and batch size for your hardware. Select a preset in Settings and it handles the rest.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;Chat Model&lt;/th&gt;
&lt;th&gt;Extraction Model&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;GPU Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;Phi4 14B&lt;/td&gt;
&lt;td&gt;Phi4 14B&lt;/td&gt;
&lt;td&gt;16K&lt;/td&gt;
&lt;td&gt;RTX 4080, RTX 5080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20 GB&lt;/td&gt;
&lt;td&gt;Phi4 14B&lt;/td&gt;
&lt;td&gt;Phi4 14B&lt;/td&gt;
&lt;td&gt;24K&lt;/td&gt;
&lt;td&gt;RTX A4000, RTX A4500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24 GB&lt;/td&gt;
&lt;td&gt;Qwen3 30B&lt;/td&gt;
&lt;td&gt;Qwen3 30B Instruct&lt;/td&gt;
&lt;td&gt;16K&lt;/td&gt;
&lt;td&gt;RTX 4090, RTX 3090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;Qwen3 30B&lt;/td&gt;
&lt;td&gt;Qwen3 30B Instruct&lt;/td&gt;
&lt;td&gt;32K&lt;/td&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48 GB&lt;/td&gt;
&lt;td&gt;Qwen3 30B&lt;/td&gt;
&lt;td&gt;Qwen3 30B Instruct&lt;/td&gt;
&lt;td&gt;48K&lt;/td&gt;
&lt;td&gt;A6000, 2x 4090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;96 GB&lt;/td&gt;
&lt;td&gt;gpt-oss 120B&lt;/td&gt;
&lt;td&gt;gpt-oss 120B&lt;/td&gt;
&lt;td&gt;48K&lt;/td&gt;
&lt;td&gt;RTX 6000 Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 GB&lt;/td&gt;
&lt;td&gt;gpt-oss 120B&lt;/td&gt;
&lt;td&gt;gpt-oss 120B&lt;/td&gt;
&lt;td&gt;64K&lt;/td&gt;
&lt;td&gt;DGX Spark, AMD Ryzen AI Max+ 395&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 24-48 GB presets use two Qwen3 tags -- pull both &lt;code&gt;qwen3:30b&lt;/code&gt; (chat) and &lt;code&gt;qwen3:30b-instruct&lt;/code&gt; (extraction) with &lt;code&gt;ollama pull&lt;/code&gt; before your first extraction.&lt;/p&gt;

&lt;p&gt;The sweet spot for most people is 24 GB. An RTX 4090 running Qwen3 30B gives you strong chat quality and solid extraction results. If you're on 16 GB, you'll still get a good experience for chat and search -- extraction quality will be noticeably lower on complex documents, but perfectly usable for straightforward material.&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%2Fjsvquwhk2qvch3q2bgdn.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%2Fjsvquwhk2qvch3q2bgdn.png" alt="LLM provider settings with Ollama configuration and VRAM preset" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;p&gt;A few things are worth knowing about how the local pipeline actually works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embeddings Are Always Local
&lt;/h3&gt;

&lt;p&gt;Here's something that surprises people: the embedding model that powers semantic search runs on CPU. It has nothing to do with Ollama or your GPU. Chaos Cypher defaults to Qwen3-Embedding-0.6B, a compact model that downloads once and runs locally via sentence-transformers. Any HuggingFace sentence-transformers model can be used, and cloud providers (OpenAI, Ollama, Gemini) are also supported.&lt;/p&gt;

&lt;p&gt;This means semantic search works even if Ollama is offline. It means you can index thousands of documents on a machine with no GPU at all. The embeddings are generated in the Neuron worker during indexing and stored in your local SQLite database (via sqlite-vec). Search queries generate an embedding on the fly, compare it against the index, and return results -- all on CPU, all local, typically in under a second.&lt;/p&gt;

&lt;p&gt;Re-ranking also runs locally -- a compact cross-encoder reorders search results by relevance before they reach the LLM, no API calls involved. Details and model options are in the &lt;a href="https://chaoscypher.com/docs/user-guide/search#re-ranking" rel="noopener noreferrer"&gt;search docs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Instance Load Balancing
&lt;/h3&gt;

&lt;p&gt;Have multiple machines with GPUs, or multiple GPUs in one workstation? Configure several Ollama instances and Chaos Cypher load-balances across them (round-robin, least-loaded, or random), with independent health checks and automatic failover -- hot-reloadable from the Settings page, no restart needed. This matters most for extraction: a 500-page document produces hundreds of chunk groups, and spreading them across two or three GPUs cuts extraction time proportionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thinking Mode
&lt;/h3&gt;

&lt;p&gt;Qwen3-style models can reason step by step in &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; tags before answering, and Chaos Cypher detects and handles this automatically -- reasoning is separated from the final response, with graceful fallback for models that don't support it. Thinking is on by default for chat and off for extraction (where it mostly adds latency), but every VRAM preset turns it off for chat too (&lt;code&gt;thinking_for_chat: false&lt;/code&gt;) to keep latency and VRAM headroom predictable -- re-enable it under Settings &amp;gt; LLM after applying a preset if you want step-by-step reasoning in chat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Reality Check
&lt;/h3&gt;

&lt;p&gt;Let's be honest about the tradeoffs, because nobody benefits from hype.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat is great locally.&lt;/strong&gt; Interactive question-answering with RAG retrieval works well on 24 GB+ hardware. The model has context from your documents, it generates coherent answers, latency is acceptable for interactive use. Streaming means you see tokens as they arrive -- the experience feels responsive even when total generation takes a few seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple extraction works well.&lt;/strong&gt; Documents with clear entity boundaries -- people's names, organization names, dates, locations -- extract reliably on local models. Legal contracts with named parties and defined obligations, research papers with cited authors and institutions, meeting notes with action items and owners.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complex extraction is where you notice the gap.&lt;/strong&gt; Dense academic papers with nuanced conceptual relationships, documents where entities are implied rather than stated, multi-hop reasoning about how concepts relate to each other -- this is where cloud models with 100B+ parameters still have a meaningful advantage. A Qwen3 30B model closes much of the gap, but the frontier cloud models keep a real lead on the hardest tasks. For many use cases, the local result is more than enough. For others, you'll want to use a cloud provider for the extraction pass and keep everything else local.&lt;/p&gt;

&lt;p&gt;The good news: Chaos Cypher lets you mix and match. Use Ollama for chat and search (where privacy matters most, since those are interactive queries about your data), and use a cloud provider for the one-time extraction pass if you need maximum quality. Or keep everything local and accept the quality tradeoff. Your call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four Providers, One Interface
&lt;/h3&gt;

&lt;p&gt;Chaos Cypher supports Ollama, OpenAI, Anthropic, and Gemini through a unified interface -- switching is a single config change, and you can mix providers per operation (Ollama for chat, a cloud model for extraction). Same extraction pipeline, same chat system, same search infrastructure either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Minimal configuration in &lt;code&gt;settings.yaml&lt;/code&gt; -- easiest to set via the Settings page in the UI; for the all-in-one container the file lives at &lt;code&gt;/data/settings.yaml&lt;/code&gt; inside the container, and for local/CLI runs it's in your &lt;a href="https://chaoscypher.com/docs/getting-started/configuration#settings-file" rel="noopener noreferrer"&gt;platform data directory&lt;/a&gt;:&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;llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;chat_provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama"&lt;/span&gt;
  &lt;span class="na"&gt;ollama_chat_model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3:30b-instruct"&lt;/span&gt;
  &lt;span class="na"&gt;ollama_num_ctx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32768&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default Ollama URL is &lt;code&gt;http://localhost:11434&lt;/code&gt;; the published image&lt;br&gt;
overrides it to &lt;code&gt;http://host.docker.internal:11434&lt;/code&gt; (via the&lt;br&gt;
&lt;code&gt;CHAOSCYPHER_OLLAMA_URL&lt;/code&gt; environment variable), which Just Works™ for the&lt;br&gt;
all-in-one container talking to a host-side Ollama. On Linux Docker Engine&lt;br&gt;
(not Docker Desktop), that name only resolves because of the&lt;br&gt;
&lt;code&gt;--add-host=host.docker.internal:host-gateway&lt;/code&gt; flag in the &lt;code&gt;docker run&lt;/code&gt;&lt;br&gt;
command above -- keep it, or set &lt;code&gt;CHAOSCYPHER_OLLAMA_URL&lt;/code&gt; to your host's LAN&lt;br&gt;
IP instead. To add multi-GPU instances, use &lt;code&gt;ollama_instances&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or skip the YAML entirely -- open the Settings page in the UI, select Ollama as your provider, pick a VRAM preset that matches your GPU, and you're done. The preset fills in the model name, context window, batch size, and extraction model automatically.&lt;/p&gt;

&lt;p&gt;Then start everything:&lt;br&gt;
&lt;/p&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;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; chaoscypher &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 443:443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; chaoscypher-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host.docker.internal:host-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/chaoscypherinc/chaoscypher:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upload a document, wait for indexing (30 seconds) and extraction (a few minutes), and you have a working knowledge graph built entirely on your hardware.&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%2F02uolct3m935zqakcdhn.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%2F02uolct3m935zqakcdhn.png" alt="Knowledge graph visualization showing extracted entities and relationships" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few tips for getting the best results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pull models before starting Chaos Cypher.&lt;/strong&gt; Run &lt;code&gt;ollama pull qwen3:30b-instruct&lt;/code&gt; (or whichever models your preset uses) before your first extraction. The Neuron worker will wait for Ollama, but pre-pulling avoids the initial download delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor VRAM usage.&lt;/strong&gt; Run &lt;code&gt;nvidia-smi&lt;/code&gt; to see how much VRAM your model is using. If you're near the limit, drop to a smaller context window or a smaller model. OOM kills during extraction are recoverable (the job retries), but they're slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with shorter documents.&lt;/strong&gt; Your first upload should be a 10-20 page document so you can see the full pipeline complete in a couple of minutes. Scale up once you're comfortable with the output quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiment with extraction models.&lt;/strong&gt; The presets pair specific extraction models with chat models. In the 24-48 GB tiers, the preset pairs the chat model with an instruct-tuned extraction variant (&lt;code&gt;qwen3:30b-instruct&lt;/code&gt;) optimized for structured output; the other tiers use one model for both. If extraction quality isn't where you want it, try the next VRAM tier up -- the jump from 8B to 30B parameters makes a significant difference in extraction accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Running everything locally is the starting point, not the ceiling.&lt;/p&gt;

&lt;p&gt;If you outgrow a single GPU, the multi-instance setup lets you spread load across multiple machines on your network -- a small GPU cluster for your team, still fully private, still no cloud dependency. Configure two or three Ollama instances on different machines, point Chaos Cypher at all of them, and extraction workloads parallelize automatically.&lt;/p&gt;

&lt;p&gt;When you do need cloud-tier quality for specific tasks, the cloud providers are there. Chaos Cypher doesn't lock you into local-only or cloud-only. You choose per-operation, per-database, whenever you want. The architecture is the same either way -- the only thing that changes is where the LLM inference happens.&lt;/p&gt;

&lt;p&gt;The privacy argument isn't really about paranoia. It's about control. Your knowledge graph is a map of everything you know -- your research, your relationships, your institutional memory. Keeping that map on your own hardware isn't a limitation. It's a feature.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>graphrag</category>
      <category>ollama</category>
      <category>python</category>
    </item>
  </channel>
</rss>
