<?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: Denis MacPherson</title>
    <description>The latest articles on DEV Community by Denis MacPherson (@dmacpherson).</description>
    <link>https://dev.to/dmacpherson</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4108762%2F9b165685-2162-4df7-b80c-aa419fa137bc.png</url>
      <title>DEV Community: Denis MacPherson</title>
      <link>https://dev.to/dmacpherson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmacpherson"/>
    <language>en</language>
    <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>
