Back to home
πΈοΈ Curiosity Crawl
Turn Wikipedia rabbit holes into interactive knowledge graphs
A full-stack app that transforms Wikipedia browsing sessions into explorable knowledge graphs. Users log page-to-page navigations, and an LLM classifies each hop into one of five relationship types β causal, tangential, definitional, biographical, or geographic. A D3.js force-directed graph renders the result in real time, with community detection that reveals thematic clusters and a surprise connection finder that surfaces the most unexpected paths through your curiosity.
The idea came from a simple observation: everyone falls down Wikipedia rabbit holes, but the path β the thread connecting Stoicism to surfing hydrodynamics β is invisible. Curiosity Crawl makes that path visible, interactive, and analyzable.
The backend is a FastAPI service backed by SQLite and NetworkX, using a write-through persistence model that keeps an in-memory graph and a durable database in sync on every mutation. The frontend is vanilla JavaScript + D3.js v7 β no build system, no framework β because the force-directed graph rendering is inherently imperative and D3 shines without an abstraction layer on top. The project includes 93 tests with full external service mocking and a 50-edge evaluation suite measuring LLM classification accuracy.

π οΈ Tech Stack
π Features
LLM-powered edge classification β Every Wikipedia hop is classified into one of 5 relationship categories using GPT-3.5 Turbo with JSON mode, exponential backoff, and a normalization map that handles model drift (e.g.,
"historical"βcausal)Interactive force-directed graph β D3.js visualization with adaptive force parameters that scale with graph size, category-coded edge colors, node sizing by visit frequency, hover highlighting that dims unconnected nodes to 15% opacity, and drag-to-pin positioning
Community detection + cluster naming β Greedy modularity algorithm identifies thematic clusters, then the LLM generates creative 3-5 word names (e.g., "Roman Philosophical Heritage"). Click a cluster card to spotlight its members on the graph
Surprise connection finder β Discovers the longest shortest paths across cluster boundaries, surfacing unexpected connections like Surfing β Fluid Dynamics β Aqueducts β Roman Empire β Marcus Aurelius β Stoicism β CBT β Aaron T. Beck
Write-through dual persistence β Every graph mutation writes to both the in-memory NetworkX DiGraph and SQLite simultaneously, so no data is lost on crash. On startup, the full graph reconstructs from the database
Session management + filtering β Multiple browsing sessions tracked independently. Filter the graph to a single session or view all sessions overlaid. Form chaining auto-fills the next "From" field after each visit
Zero-config demo mode β
seed_demo.py --mockpopulates 3 sessions with 22 pre-labeled edges using hard-coded data, no API key required. Reviewers can clone and see a working graph in under 60 seconds50-edge evaluation suite β Manually labeled dataset with
also_acceptablefields for genuinely ambiguous edges. Reports strict accuracy, lenient accuracy, per-category precision/recall/F1, and a 5x5 confusion matrix
π§ββοΈ Challenges
Dual-write consistency β The GraphManager maintains both a NetworkX graph (for fast traversals) and SQLite (for durability). Every
add_nodeandadd_edgewrites to both stores atomically. The tricky part was ensuring foreign key constraints in SQLite were satisfied during write-through β the in-memory graph doesn't enforce referential integrity, but the database does. This surfaced in testing when edges referenced sessions that hadn't been created in the test DB yetLLM output reliability at scale β GPT-3.5 Turbo occasionally returns categories outside the defined taxonomy (
"historical","conceptual","related"). Built a normalization map to correct common drift, defaulting to"tangential"for unmapped responses and"unknown"when all retries are exhausted. The retry strategy uses exponential backoff[1s, 3s, 9s]with doubled delays for rate limit errors specificallyD3 force layout scaling β Fixed force parameters that look great with 15 nodes produce an unreadable hairball at 80+. Implemented adaptive parameters: charge strength weakens from -200 to -100, link distance stretches from 80px to 120px, and labels are hidden for low-degree nodes above 30 total β all keyed to current node count
Framework swap mid-build β The spec was written for Flask, but we switched to FastAPI after Phase 1. Because the data layer was framework-agnostic from day one (no
flask.g, no request-scoped connections), the swap only required rewritingmain.py, the Dockerfile, and one test. A clean architecture boundary turned a risky pivot into a 30-minute taskTransitive dependency conflict β FastAPI's Starlette pinned
httpx <0.28, but the OpenAI SDK pulled inhttpx 0.28+, which removed theapp=kwarg the test client depends on. Diagnosed by tracing theTypeErrorthrough starlette's TestClient constructor and resolved by pinninghttpx >=0.23,<0.28as a dev dependency
π§ Learnings
Architecture decisions compound β Debugging cycles decreased with each phase: Phase 1 had 4, Phase 2 had 2, Phase 3 had 1, Phases 4-6 had 0. Early investments in test isolation, service boundaries, and framework-agnostic data layers prevented entire classes of bugs later. The Phase 1 decision to enable
PRAGMA foreign_keys = ONcaught a data integrity issue in Phase 3 that would have silently corrupted production dataEvaluate subjective AI output honestly β The eval suite includes an
also_acceptablefield because "Marcus Aurelius β Roman Empire" is legitimately both biographical and geographic. Forcing single labels inflates error counts and makes evals less useful. Reporting both strict and lenient accuracy gives a more honest picture of model performanceDomain knowledge beats algorithmic brute force β The surprise finder's key optimization isn't sampling (though it does that above 200 nodes) β it's skipping intra-community pairs. Paths within a cluster aren't surprising by definition. This community-aware filtering produces better results while also reducing computation by ~33%
Portfolio projects need a zero-config demo path β If seeing the project requires an API key and 10 minutes of setup, most reviewers bounce. The
--mockseed mode bridges the gap betweengit cloneand a populated, interactive graph. This was the single most impactful feature for portfolio presentationBuild the frontend last against a stable API β Phases 1-4 built and fully tested the backend (93 tests, all passing). Phase 5 was pure greenfield frontend with zero debugging cycles, because every API contract was already proven. No ambiguity about whether a rendering bug was a frontend or backend problem