Jennifer Nguyen

Bootwitch

Scientist building AI and research tools
10+ years in immunology research · Building with AI since 2024
Bootwitch / Interactive demo
Simulated session
Files
Terminal
Welcome to Bootwitch.
Projects / wikigen-technical-notes

Attractor — Knowledge Graph - Technical

wikigencontext-and-memory

README · ARCHITECTURE · TECHNICAL · source


1. Extraction is per-conversation; consolidation is global

Analysing each conversation in isolation keeps extraction cheap and independent, but the same idea comes back phrased differently every time — local JSON state, local JSON state persistence, local JSON storage, local JSON state management. Four nodes for one idea.

One model call per conversation returns 3–5 concepts. A single further call over the full sorted concept list returns a {original: canonical} mapping containing only the names being changed, applied before the graph is built.

Because near-synonyms share neighbours, the force-directed layout correctly stacks them and the graph becomes a pile of overlapping labels. No layout tuning fixes this; the merge has to happen upstream of the graph.

That adds one model call per run over the whole corpus, making it the most expensive single call in the pipeline.

The prompt is deliberately conservative: merge only genuine synonyms; when in doubt leave a name alone — because the two failure modes are not symmetric. A missed merge shows up as visible duplicate labels. A wrong merge silently destroys the distinction the graph exists to show. Conservatism means duplicates do survive.


2. Layout is computed once and held fixed

The animation reveals concepts chronologically. Recomputing layout per frame as nodes arrive produces a graph that writhes — every node moves every frame, nothing can be tracked visually, and the animation shows the layout algorithm rather than the history.

compute_layout runs once on the complete graph with seed=42. Every GIF frame, the PNG, and the HTML use those coordinates.

A node's position then means the same thing in frame 1 and frame 200, which is the only reason the animation is readable.

Early frames show nodes in positions determined by relationships that have not appeared yet. The layout is, strictly, showing the future.

The fixed seed makes a repeated layout reproducible for the same graph. In the model comparison, the concepts and graph structure change too, so positions can still differ. The saved concept counts are a better way to compare extraction than position alone.


3. Disconnected components are packed, not sprung

With roughly one cluster per conversation topic, the graph is usually disconnected. spring_layout flings whole components apart with nothing between them, so each cluster collapses into an unreadable knot in a mostly-empty frame.

I lay each connected component out on its own, then pack the resulting boxes with a shelf algorithm — left to right, wrapping to a new row, row height set by the tallest box. Box size scales with sqrt(node count).

A uniform grid is the obvious implementation and it fails in the worst possible direction: it gives a forty-node cluster the same canvas as a three-node one, compressing exactly the part of the picture you most wanted to read. The small clusters look fine and the one that matters is illegible.

Packing has no notion of relatedness between components, so neighbouring boxes are neighbours by packing order, not by meaning.

The shelf algorithm leaves ragged whitespace at the end of rows. That is acceptable here; a better packer is not the bottleneck.


4. The noise filter runs before the API call

A corpus of real transcripts contains stack traces, terminal dumps and two-line exchanges. Extracting concepts from them costs money and returns nothing.

is_noisy drops a conversation if more than 55% of its lines match noise patterns, or if it has fewer than 500 characters of content. It runs before the model call.

The saving is the entire point; filtering after extraction would cost the same and only tidy the graph.

Both thresholds are tuned against one corpus — mine — and are the first two numbers anyone else should change.

A conversation that is genuinely about debugging output is indistinguishable from noise by this filter, and gets dropped. In a corpus about building software that is a real category being silently excluded.


A note on model-dependent failure

Three separate comments in extract_concepts document bugs of the same class: code that works on one model and silently fails on another. A max_tokens generous for Haiku truncates Opus mid-JSON; a content[0] correct for a non-reasoning model returns a thinking block with no .text; a workaround for an SDK change is itself what a newer model rejects.

Each produced an empty concept list rather than an exception — a conversation that quietly contributed nothing to the graph. Degrading to a smaller graph instead of crashing is the right behavior and is also why these took so long to notice.



Project overview · All projects